diff --git a/client/canonical_eventlog.go b/client/canonical_eventlog.go new file mode 100644 index 000000000..65d6f28b8 --- /dev/null +++ b/client/canonical_eventlog.go @@ -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 +} diff --git a/client/canonical_eventlog_test.go b/client/canonical_eventlog_test.go new file mode 100644 index 000000000..fb690a84f --- /dev/null +++ b/client/canonical_eventlog_test.go @@ -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") + } +} diff --git a/client/costlv.go b/client/costlv.go new file mode 100644 index 000000000..2e3654d21 --- /dev/null +++ b/client/costlv.go @@ -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 +}