Skip to content

Commit

Permalink
Add basic Canonical Event Log (CEL) operations
Browse files Browse the repository at this point in the history
Defined CEL TLV struct, added encoding/decoding functions
  • Loading branch information
jkl73 committed Nov 3, 2021
1 parent 8a35101 commit 61db5fe
Show file tree
Hide file tree
Showing 3 changed files with 364 additions and 0 deletions.
266 changes: 266 additions & 0 deletions client/canonical_eventlog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
package client

import (
"bytes"
"crypto"
"encoding/binary"
"fmt"
"io"

"github.com/google/go-tpm/tpm2"
)

var hashAlgoMapping = map[crypto.Hash]tpm2.Algorithm{
crypto.SHA1: tpm2.AlgSHA1,
crypto.SHA256: tpm2.AlgSHA256,
crypto.SHA384: tpm2.AlgSHA384,
crypto.SHA512: tpm2.AlgSHA512,
}

const (
// CEL spec 5.1
recNumTagValue uint8 = 0
pcrTagValue uint8 = 1
nvIndexTagValue uint8 = 2
digestsTagValue uint8 = 3

tlvTagFieldLength int = 1
tlvLengthFieldLength int = 4

recNumValueLength uint32 = 8 // support up to 2^64 records
pcrValueLength uint32 = 1 // support up to 256 PCRs
)

// TLV definition according to CEL spec, page 16
type TLV struct {
Tag uint8
Length uint32 // big-endian when encoding
Value []byte // with size of Length
}

// TLVMarshal marhsal a TLV to a byte slice
func TLVMarshal(tlv TLV) []byte {
buf := make([]byte, tlv.Length+uint32(tlvTagFieldLength)+uint32(tlvLengthFieldLength))

buf[0] = tlv.Tag
binary.BigEndian.PutUint32(buf[tlvTagFieldLength:], tlv.Length)
copy(buf[tlvTagFieldLength+tlvLengthFieldLength:], tlv.Value)

return buf
}

// TLVUnmarshal reads and parse the first TLV from the bytes buffer.
func TLVUnmarshal(buf *bytes.Buffer) (TLV, error) {
var tlv TLV
var err error

// read tag
tlv.Tag, err = buf.ReadByte()
if err != nil {
return TLV{}, err
}

// read length
b := make([]byte, tlvLengthFieldLength)
l, err := buf.Read(b)

if err != nil || l < tlvLengthFieldLength {
return TLV{}, io.EOF
}
tlv.Length = binary.BigEndian.Uint32(b)

// read value
tlv.Value = make([]byte, tlv.Length)
l, err = buf.Read(tlv.Value)
if err != nil || l < int(tlv.Length) {
return TLV{}, io.EOF
}
return tlv, nil
}

// CELR represents a Canoical Eventlog Record
type CELR struct {
RecNum TLV
PCR TLV
Digests TLV
Content TLV
}

// CELREventData is a interface for the content in CELR.
type CELREventData interface {
GenerateDigest(crypto.Hash) ([]byte, error)
GetTLV() TLV
}

// CEL represents a Canonical Eventlog, which contains a list of CELR
type CEL struct {
Records []CELR
}

// AppendEvent appends a new record to the CEL.
func (c *CEL) AppendEvent(tpm io.ReadWriteCloser, pcr int, hashAlgos []crypto.Hash, payload CELREventData) error {
digestsMap := make(map[tpm2.Algorithm][]byte)
for _, hashAlgo := range hashAlgos {
digest, err := payload.GenerateDigest(hashAlgo)
if err != nil {
return err
}
digestsMap[hashAlgoMapping[hashAlgo]] = digest

// TODO: extend the digest to TPM PCR
}

celr, err := createCELR(uint64(len(c.Records)), uint8(pcr), digestsMap, payload)
if err != nil {
return err
}

c.Records = append(c.Records, celr)
return nil
}

func createRecNumField(recNum uint64) TLV {
payload := make([]byte, recNumValueLength)
binary.BigEndian.PutUint64(payload, recNum)
return TLV{recNumTagValue, recNumValueLength, payload}
}

// UnmarshalRecNum takes in a TLV with its tag equals to the recnum tag value (0), and
// return its record number.
func UnmarshalRecNum(tlv TLV) (uint64, error) {
if tlv.Tag != recNumTagValue {
return 0, fmt.Errorf("tag of the TLV indicates it is not a recnum field")
}
return binary.BigEndian.Uint64(tlv.Value), nil
}

func createPCRField(pcrNum uint8) TLV {
return TLV{pcrTagValue, pcrValueLength, []byte{pcrNum}}
}

// UnmarshalPCR takes in a TLV with its tag equals to the PCR tag value (3), and
// return its PCR number.
func UnmarshalPCR(tlv TLV) (pcrNum uint8, err error) {
if tlv.Tag != pcrTagValue {
return 0, fmt.Errorf("tag of the TLV indicates it is not a PCR field")
}
return tlv.Value[0], nil
}

func createDigestField(digestMap map[tpm2.Algorithm][]byte) TLV {
var buf bytes.Buffer

for hashAlgo, hash := range digestMap {
singledigest := TLV{uint8(hashAlgo), uint32(len(hash)), hash}
_, err := buf.Write(TLVMarshal(singledigest))
if err != nil {
panic(err)
}
}

return TLV{digestsTagValue, uint32(buf.Len()), buf.Bytes()}
}

// UnmarshalDigests takes in a TLV with its tag equals to the digests Tag value (3), and
// return its digests content in a map, the key is its TPM hash algorithm.
func UnmarshalDigests(tlv TLV) (digestsMap map[tpm2.Algorithm][]byte, err error) {
if tlv.Tag != digestsTagValue {
return nil, fmt.Errorf("tag of the TLV indicates it doesn't contain digests")
}

buf := bytes.NewBuffer(tlv.Value)
digestsMap = make(map[tpm2.Algorithm][]byte)

for buf.Len() > 0 {
digestTLV, err := TLVUnmarshal(buf)
if err == io.EOF {
return nil, fmt.Errorf("buffer ends unexpectedly")
}
digestsMap[tpm2.Algorithm(digestTLV.Tag)] = digestTLV.Value
}
return digestsMap, nil
}

func createCELR(recNum uint64, pcr uint8, digestsmap map[tpm2.Algorithm][]byte, event CELREventData) (celr CELR, err error) {
recnumField := createRecNumField(recNum)
pcrField := createPCRField(pcr)

digestField := createDigestField(digestsmap)
celr = CELR{recnumField, pcrField, digestField, event.GetTLV()}

return celr, nil
}

// EncodeCELR encodes the CELR to bytes according to the CEL spec and write them
// to the byte byffer.
func (r *CELR) EncodeCELR(buf *bytes.Buffer) error {
// encode CEL and write the binary eventlog to the buf
recnumField := TLVMarshal(TLV(r.RecNum))
pcrField := TLVMarshal(TLV(r.PCR))
digestsField := TLVMarshal(TLV(r.Digests))
eventField := TLVMarshal(TLV(r.Content))

buf.Write(recnumField)
buf.Write(pcrField)
buf.Write(digestsField)
buf.Write(eventField)
return nil
}

// EncodeCEL encodes the CEL to bytes according to the CEL spec and write them
// to the byte buffer.
func (c *CEL) EncodeCEL(buf *bytes.Buffer) error {
for _, record := range c.Records {
if err := record.EncodeCELR(buf); err != nil {
return err
}
}
return nil
}

// DecodeToCEL will read the buf for CEL, will return err if the buffer
// is not complete.
func DecodeToCEL(buf *bytes.Buffer) (CEL, error) {
var cel CEL
for buf.Len() > 0 {
celr, err := DecodeToCELR(buf)
if err == io.EOF {
return CEL{}, fmt.Errorf("buffer ends unexpectedly")
}
cel.Records = append(cel.Records, celr)
}
return cel, nil
}

// DecodeToCELR will read the buf for the next CELR, will return err if
// failed to unmarshal the TLV from the buffer.
func DecodeToCELR(buf *bytes.Buffer) (CELR, error) {
recNum, err := TLVUnmarshal(buf)
if err != nil {
return CELR{}, err
}

pcr, err := TLVUnmarshal(buf)
if err != nil {
return CELR{}, err
}

digests, err := TLVUnmarshal(buf)
if err != nil {
return CELR{}, err
}

content, err := TLVUnmarshal(buf)
if err != nil {
return CELR{}, err
}

celr := CELR{
recNum,
pcr,
digests,
content,
}

return celr, nil
}
69 changes: 69 additions & 0 deletions client/canonical_eventlog_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package client

import (
"bytes"
"crypto"
"reflect"
"testing"

"github.com/google/go-tpm/tpm2"
)

func TestCELEncodingDecoding(t *testing.T) {
hashAlgoList := []crypto.Hash{crypto.SHA256, crypto.SHA1}
cel := &CEL{}

someEvent := make([]byte, 10)
cosEvent := COSTLV{COSTagValue, uint32(10), someEvent}
cel.AppendEvent(nil, 13, hashAlgoList, cosEvent)

someEvent2 := make([]byte, 10)
cosEvent2 := COSTLV{COSTagValue, uint32(10), someEvent2}
cel.AppendEvent(nil, 14, hashAlgoList, cosEvent2)

var buf bytes.Buffer
err := cel.EncodeCEL(&buf)
if err != nil {
t.Fatalf(err.Error())
}

decodedcel, err := DecodeToCEL(&buf)
if err != nil {
t.Fatalf(err.Error())
}

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

if recnum, err := UnmarshalRecNum(decodedcel.Records[0].RecNum); err != nil || recnum != uint64(0) {
t.Errorf("recnum mismatch")
}
if recnum, err := UnmarshalRecNum(decodedcel.Records[1].RecNum); err != nil || recnum != uint64(1) {
t.Errorf("recnum mismatch")
}

if pcr, err := UnmarshalPCR(decodedcel.Records[0].PCR); err != nil || pcr != uint8(13) {
t.Errorf("pcr value mismatch")
}
if pcr, err := UnmarshalPCR(decodedcel.Records[1].PCR); err != nil || pcr != uint8(14) {
t.Errorf("pcr value mismatch")
}

digestsMap, err := UnmarshalDigests(decodedcel.Records[0].Digests)
if err != nil {
t.Error(err)
}

if len(digestsMap[tpm2.AlgSHA256]) != 32 {
t.Errorf("SHA256 digest length doesn't match")
}

if len(digestsMap[tpm2.AlgSHA1]) != 20 {
t.Errorf("SHA1 digest length doesn't match")
}

if reflect.DeepEqual(decodedcel.Records, (*cel).Records) == false {
t.Errorf("decoded CEL doesn't equal to the original one")
}
}
29 changes: 29 additions & 0 deletions client/costlv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package client

import "crypto"

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

// COSTLV is a specific event type created for the COS (Google Container-Optimized OS),
// used as a CEL content.
type COSTLV TLV

// GetTLV returns the TLV representation of the COS TLV.
func (c COSTLV) GetTLV() TLV {
return TLV(c)
}

// 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) {
hash := hashAlgo.New()
_, err := hash.Write(TLVMarshal(c.GetTLV()))
if err != nil {
return nil, err
}
return hash.Sum(nil), nil
}

0 comments on commit 61db5fe

Please sign in to comment.