From 3a92951c6811100f5c83f349133b2b6b66195b75 Mon Sep 17 00:00:00 2001 From: Jiankun Lu Date: Tue, 2 Nov 2021 14:08:27 -0700 Subject: [PATCH] Add basic Canonical Event Log (CEL) operations Defined CEL TLV struct, added encoding/decoding functions --- cel/canonical_eventlog.go | 341 +++++++++++++++++++++++++++++++++ cel/canonical_eventlog_test.go | 71 +++++++ cel/cos_tlv.go | 34 ++++ 3 files changed, 446 insertions(+) create mode 100644 cel/canonical_eventlog.go create mode 100644 cel/canonical_eventlog_test.go create mode 100644 cel/cos_tlv.go diff --git a/cel/canonical_eventlog.go b/cel/canonical_eventlog.go new file mode 100644 index 000000000..8293a3311 --- /dev/null +++ b/cel/canonical_eventlog.go @@ -0,0 +1,341 @@ +// Package cel contains some basic operations of Canonical Eventlog. +// Based on Canonical EventLog Spec (Draft) Version: TCG_IWG_CEL_v1_r0p37. +package cel + +import ( + "bytes" + "crypto" + "encoding/binary" + "fmt" + "io" + + "github.com/google/go-tpm/tpm2" +) + +var cryptoHashToTpmAlg = map[crypto.Hash]tpm2.Algorithm{ + crypto.SHA1: tpm2.AlgSHA1, + crypto.SHA256: tpm2.AlgSHA256, + crypto.SHA384: tpm2.AlgSHA384, + crypto.SHA512: tpm2.AlgSHA512, +} +var tpmAlgToCryptoHash = map[tpm2.Algorithm]crypto.Hash{ + tpm2.AlgSHA1: crypto.SHA1, + tpm2.AlgSHA256: crypto.SHA256, + tpm2.AlgSHA384: crypto.SHA384, + tpm2.AlgSHA512: crypto.SHA512, +} + +const ( + // CEL spec 5.1 + recnumTypeValue uint8 = 0 + pcrTypeValue uint8 = 1 + // nvIndexTagValue uint8 = 2 // nvindex field is not supported yet + digestsTypeValue uint8 = 3 + + tlvTypeFieldLength 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 TCG_IWG_CEL_v1_r0p37, page 16. +type TLV struct { + Type uint8 + Length uint32 // big-endian when encoding + Value []byte // with size of Length +} + +// Marshal marhsals a TLV to a byte slice. +func (tlv *TLV) Marshal() ([]byte, error) { + if tlv.Length != uint32(len(tlv.Value)) { + return nil, fmt.Errorf("length of tlv.Value [%d] doesn't equal to tlv.Length [%d]", + len(tlv.Value), tlv.Length) + } + + buf := make([]byte, tlv.Length+uint32(tlvTypeFieldLength)+uint32(tlvLengthFieldLength)) + + buf[0] = tlv.Type + binary.BigEndian.PutUint32(buf[tlvTypeFieldLength:], tlv.Length) + copy(buf[tlvTypeFieldLength+tlvLengthFieldLength:], tlv.Value) + + return buf, nil +} + +// TLVUnmarshal reads and parse the first TLV from the bytes buffer. The function will +// return io.EOF if the buf ends unexpectedly or cannot filled the TLV. +func TLVUnmarshal(buf *bytes.Buffer) (TLV, error) { + var tlv TLV + var err error + + // read type + tlv.Type, err = buf.ReadByte() + if err != nil { + return TLV{}, err + } + + // read length + b := make([]byte, tlvLengthFieldLength) + l, err := buf.Read(b) + if err != nil { + return TLV{}, err + } + if 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 { + return TLV{}, err + } + if l < int(tlv.Length) { + return TLV{}, io.EOF + } + return tlv, nil +} + +// Record represents a Canonical Eventlog Record. +type Record struct { + RECNUM TLV + PCR TLV + Digests TLV + Content TLV +} + +// Content is a interface for the content in CELR. +type Content interface { + GenerateDigest(crypto.Hash) ([]byte, error) + GetTLV() TLV +} + +// CEL represents a Canonical Eventlog, which contains a list of CELR. +type CEL struct { + Records []Record +} + +// AppendEvent appends a new record to the CEL. +func (c *CEL) AppendEvent(tpm io.ReadWriteCloser, pcr int, hashAlgos []crypto.Hash, event Content) error { + if len(hashAlgos) == 0 { + return fmt.Errorf("need to specifiy at least one hash algorithm") + } + digestsMap := make(map[crypto.Hash][]byte) + + for _, hashAlgo := range hashAlgos { + digest, err := event.GenerateDigest(hashAlgo) + if err != nil { + return err + } + digestsMap[hashAlgo] = digest + + // TODO: extend the digest to TPM PCR + } + + celr, err := createCELR(uint64(len(c.Records)), uint8(pcr), digestsMap, event) + if err != nil { + return err + } + + c.Records = append(c.Records, celr) + return nil +} + +func createRecNumField(recNum uint64) TLV { + value := make([]byte, recnumValueLength) + binary.BigEndian.PutUint64(value, recNum) + return TLV{recnumTypeValue, recnumValueLength, value} +} + +// 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.Type != recnumTypeValue { + return 0, fmt.Errorf("tag of the TLV [%d] indicates it is not a recnum field [%d]", + tlv.Type, recnumTypeValue) + } + return binary.BigEndian.Uint64(tlv.Value), nil +} + +func createPCRField(pcrNum uint8) TLV { + return TLV{pcrTypeValue, pcrValueLength, []byte{pcrNum}} +} + +// UnmarshalPCR takes in a TLV with its tag equals to the PCR tag value (1), and +// return its PCR number. +func UnmarshalPCR(tlv TLV) (pcrNum uint8, err error) { + if tlv.Type != pcrTypeValue { + return 0, fmt.Errorf("tag of the TLV [%d] indicates it is not a PCR field [%d]", + tlv.Type, pcrTypeValue) + } + return tlv.Value[0], nil +} + +func createDigestField(digestMap map[crypto.Hash][]byte) (TLV, error) { + var buf bytes.Buffer + for hashAlgo, hash := range digestMap { + if len(hash) != hashAlgo.Size() { + return TLV{}, fmt.Errorf("digest length [%d] doesn't match the expected length [%d] for the hash algorithm", + len(hash), hashAlgo.Size()) + } + if _, ok := cryptoHashToTpmAlg[hashAlgo]; !ok { + return TLV{}, fmt.Errorf("digest algorithm not supported [%s]", hashAlgo) + } + singledigestTLV := TLV{uint8(cryptoHashToTpmAlg[hashAlgo]), uint32(len(hash)), hash} + d, err := singledigestTLV.Marshal() + if err != nil { + return TLV{}, err + } + _, err = buf.Write(d) + if err != nil { + return TLV{}, err + } + } + return TLV{digestsTypeValue, uint32(buf.Len()), buf.Bytes()}, nil +} + +// 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[crypto.Hash][]byte, err error) { + if tlv.Type != digestsTypeValue { + return nil, fmt.Errorf("tag of the TLV indicates it doesn't contain digests") + } + + buf := bytes.NewBuffer(tlv.Value) + digestsMap = make(map[crypto.Hash][]byte) + + for buf.Len() > 0 { + digestTLV, err := TLVUnmarshal(buf) + if err == io.EOF { + return nil, fmt.Errorf("buffer ends unexpectedly") + } else if err != nil { + return nil, err + } + digestsMap[tpmAlgToCryptoHash[tpm2.Algorithm(digestTLV.Type)]] = digestTLV.Value + } + return digestsMap, nil +} + +func createCELR(recNum uint64, pcr uint8, digestsmap map[crypto.Hash][]byte, event Content) (celr Record, err error) { + recnumField := createRecNumField(recNum) + pcrField := createPCRField(pcr) + + digestField, err := createDigestField(digestsmap) + if err != nil { + return Record{}, err + } + celr = Record{recnumField, pcrField, digestField, event.GetTLV()} + + return celr, nil +} + +// EncodeCELR encodes the CELR to bytes according to the CEL spec and write them +// to the bytes byffer. +func (r *Record) EncodeCELR(buf *bytes.Buffer) error { + recnumField, err := r.RECNUM.Marshal() + if err != nil { + return err + } + pcrField, err := r.PCR.Marshal() + if err != nil { + return err + } + digestsField, err := r.Digests.Marshal() + if err != nil { + return err + } + eventField, err := r.Content.Marshal() + if err != nil { + return err + } + _, err = buf.Write(recnumField) + if err != nil { + return err + } + _, err = buf.Write(pcrField) + if err != nil { + return err + } + _, err = buf.Write(digestsField) + if err != nil { + return err + } + _, err = buf.Write(eventField) + if err != nil { + return err + } + return nil +} + +// EncodeCEL encodes the CEL to bytes according to the CEL spec and write them +// to the bytes 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") + } + if err != nil { + return CEL{}, err + } + 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 a correct CELR TLV from the buffer. +func DecodeToCELR(buf *bytes.Buffer) (Record, error) { + recnum, err := TLVUnmarshal(buf) + if err != nil { + return Record{}, err + } + if recnum.Type != recnumTypeValue { + return Record{}, fmt.Errorf("recnum TLV doesn't have the correct type [%d], got [%d]", + recnumTypeValue, recnum.Type) + } + + pcr, err := TLVUnmarshal(buf) + if err != nil { + return Record{}, err + } + if pcr.Type != pcrTypeValue { + return Record{}, fmt.Errorf("pcr TLV doesn't have the correct type [%d], got [%d]", + pcrTypeValue, pcr.Type) + } + + digests, err := TLVUnmarshal(buf) + if err != nil { + return Record{}, err + } + if digests.Type != digestsTypeValue { + return Record{}, fmt.Errorf("digests TLV doesn't have the correct type [%d], got [%d]", + digestsTypeValue, digests.Type) + } + + content, err := TLVUnmarshal(buf) + if err != nil { + return Record{}, err + } + celr := Record{ + recnum, + pcr, + digests, + content, + } + + return celr, nil +} diff --git a/cel/canonical_eventlog_test.go b/cel/canonical_eventlog_test.go new file mode 100644 index 000000000..b5056c957 --- /dev/null +++ b/cel/canonical_eventlog_test.go @@ -0,0 +1,71 @@ +package cel + +import ( + "bytes" + "crypto" + "reflect" + "testing" +) + +func TestCELEncodingDecoding(t *testing.T) { + hashAlgoList := []crypto.Hash{crypto.SHA256, crypto.SHA1, crypto.SHA512} + cel := &CEL{} + + someEvent := make([]byte, 10) + cosEvent := CosTlv{CosTypeValue, uint32(10), someEvent} + err := cel.AppendEvent(nil, 13, hashAlgoList, cosEvent) + if err != nil { + t.Fatal(err.Error()) + } + + cosEvent2 := CosTlv{CosTypeValue, uint32(10), someEvent} + err = cel.AppendEvent(nil, 14, hashAlgoList, cosEvent2) + if err != nil { + t.Fatal(err.Error()) + } + + cosEvent3 := CosTlv{CosTypeValue, uint32(15), someEvent} + err = cel.AppendEvent(nil, 15, hashAlgoList, cosEvent3) + if err == nil { + t.Fatal("Expect the append to fail because the length of the event doesn't match") + } + + 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[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) == false { + t.Errorf("decoded CEL doesn't equal to the original one") + } +} diff --git a/cel/cos_tlv.go b/cel/cos_tlv.go new file mode 100644 index 000000000..0bd00f242 --- /dev/null +++ b/cel/cos_tlv.go @@ -0,0 +1,34 @@ +package cel + +import "crypto" + +const ( + // CosTypeValue indicate the CELR event is a COS content + // TODO: the value needs to be reserved in the CEL spec + CosTypeValue 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() + t := c.GetTLV() + b, err := t.Marshal() + if err != nil { + return nil, err + } + _, err = hash.Write(b) + if err != nil { + return nil, err + } + return hash.Sum(nil), nil +}