squash(pcr0tool): Add packages "bytes", "check", "errors", "mathtools", "ostools" and "tpmeventlog"

This commit is contained in:
Dmitrii Okunev 2021-03-18 09:00:16 +00:00 committed by Christopher Meis
parent fa79b4903a
commit e5b1ee4c0a
17 changed files with 762 additions and 0 deletions

16
pkg/bytes/README.md Normal file
View File

@ -0,0 +1,16 @@
```
goos: linux
goarch: amd64
BenchmarkIsZeroFilled/size_0/default-8 1000000000 3.03 ns/op 0 B/op 0 allocs/op
BenchmarkIsZeroFilled/size_0/simple-8 1000000000 2.93 ns/op 0 B/op 0 allocs/op
BenchmarkIsZeroFilled/size_1/default-8 1000000000 4.74 ns/op 0 B/op 0 allocs/op
BenchmarkIsZeroFilled/size_1/simple-8 1000000000 3.31 ns/op 0 B/op 0 allocs/op
BenchmarkIsZeroFilled/size_256/default-8 212255557 28.4 ns/op 0 B/op 0 allocs/op
BenchmarkIsZeroFilled/size_256/simple-8 71001369 86.8 ns/op 0 B/op 0 allocs/op
BenchmarkIsZeroFilled/size_65536/default-8 1466428 3961 ns/op 0 B/op 0 allocs/op
BenchmarkIsZeroFilled/size_65536/simple-8 308932 19780 ns/op 0 B/op 0 allocs/op
BenchmarkIsZeroFilled/size_1048576/default-8 106068 65791 ns/op 0 B/op 0 allocs/op
BenchmarkIsZeroFilled/size_1048576/simple-8 17924 335547 ns/op 0 B/op 0 allocs/op
PASS
ok _/home/xaionaro/go/src/github.com/9elements/converged-security-suite/v2/cmd/pcr0tool/pkg/bytes 63.711s
```

View File

@ -0,0 +1,11 @@
package bytes
//go:nosplit
func isZeroFilledSimple(b []byte) bool {
for _, v := range b {
if v != 0 {
return false
}
}
return true
}

View File

@ -0,0 +1,32 @@
// +build amd64
package bytes
import (
"reflect"
"unsafe"
)
// IsZeroFilled returns true if b consists of zeros only.
func IsZeroFilled(b []byte) bool {
hdr := (*reflect.SliceHeader)((unsafe.Pointer)(&b))
data := hdr.Data
length := hdr.Len
if data&0x07 != 0 {
// the data is not aligned, fallback to a simple way
return isZeroFilledSimple(b)
}
dataEnd := hdr.Data + uintptr(length)
dataWordsEnd := dataEnd & ^uintptr(0x07)
for ; data < dataWordsEnd; data += 8 {
if *(*uint64)(unsafe.Pointer(data)) != 0 {
return false
}
}
for ; data < dataEnd; data++ {
if *(*uint8)(unsafe.Pointer(data)) != 0 {
return false
}
}
return true
}

View File

@ -0,0 +1,9 @@
// +build !amd64
package bytes
// IsZeroFilled returns true if b consists of zeros only.
//go:nosplit
func IsZeroFilled(b []byte) bool {
return isZeroFilledSimple(b)
}

View File

@ -0,0 +1,28 @@
package bytes
import (
"fmt"
"testing"
)
func BenchmarkIsZeroFilled(b *testing.B) {
for _, size := range []uint64{0, 1, 256, 65536, 1 << 20} {
d := make([]byte, size)
b.Run(fmt.Sprintf("size_%d", size), func(b *testing.B) {
b.Run("default", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
IsZeroFilled(d)
}
})
b.Run("simple", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
isZeroFilledSimple(d)
}
})
})
}
}

124
pkg/bytes/range.go Normal file
View File

@ -0,0 +1,124 @@
package bytes
import (
"fmt"
"sort"
"strings"
)
// Range defines is a generic bytes range headers.
type Range struct {
Offset uint64
Length uint64
}
func (r Range) String() string {
return fmt.Sprintf(`{"Offset":"0x%x", "Length":"0x%x"}`, r.Offset, r.Length)
}
// Intersect returns True if ranges "r" and "cmp" has at least
// one byte with the same offset.
func (r Range) Intersect(cmp Range) bool {
if r.Length == 0 || cmp.Length == 0 {
return false
}
startIdx0 := r.Offset
startIdx1 := cmp.Offset
endIdx0 := startIdx0 + r.Length
endIdx1 := startIdx1 + cmp.Length
if endIdx0 <= startIdx1 {
return false
}
if startIdx0 >= endIdx1 {
return false
}
return true
}
// Ranges is a helper to manipulate multiple `Range`-s at once
type Ranges []Range
func (s Ranges) String() string {
r := make([]string, 0, len(s))
for _, oneRange := range s {
r = append(r, oneRange.String())
}
return `[` + strings.Join(r, `, `) + `]`
}
// Sort sorts the slice by field Offset
func (s Ranges) Sort() {
sort.Slice(s, func(i, j int) bool {
return s[i].Offset < s[j].Offset
})
}
// MergeRanges just merges ranges which has distance less or equal to
// mergeDistance.
//
// Warning: should be called only on sorted ranges!
func MergeRanges(in Ranges, mergeDistance uint64) Ranges {
if len(in) < 2 {
return in
}
var result Ranges
entry := in[0]
for _, nextEntry := range in[1:] {
// merge "nextEntry" to "entry" if the distance is lower or equal to
// mergeDistance.
if entry.Offset+entry.Length+mergeDistance >= nextEntry.Offset {
entry.Length = (nextEntry.Offset - entry.Offset) + nextEntry.Length
continue
}
result = append(result, entry)
entry = nextEntry
}
result = append(result, entry)
return result
}
// SortAndMerge sorts the slice (by field Offset) and the merges ranges
// which could be merged.
func (s *Ranges) SortAndMerge() {
// See also TestDiffEntriesSortAndMerge
if len(*s) < 2 {
return
}
s.Sort()
*s = MergeRanges(*s, 0)
}
// Compile returns the bytes from `b` which are referenced by `Range`-s `s`.
func (s Ranges) Compile(b []byte) []byte {
var result []byte
for _, r := range s {
result = append(result, b[r.Offset:r.Offset+r.Length]...)
}
return result
}
// IsIn returns if the index is covered by this ranges
func (s Ranges) IsIn(index uint64) bool {
for _, r := range s {
startIdx := r.Offset
endIdx := r.Offset + r.Length
// `startIdx` is inclusive, while `endIdx` is exclusive.
// The same as usual slice indices works:
//
// slice[startIdx:endIdx]
if startIdx <= index && index < endIdx {
return true
}
}
return false
}

55
pkg/bytes/range_test.go Normal file
View File

@ -0,0 +1,55 @@
package bytes
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestRangesSortAndMerge(t *testing.T) {
t.Run("nothing_to_merge", func(t *testing.T) {
entries := Ranges{{
Offset: 2,
Length: 1,
}, {
Offset: 0,
Length: 1,
}}
entries.SortAndMerge()
require.Equal(t, Ranges{{
Offset: 0,
Length: 1,
}, {
Offset: 2,
Length: 1,
}}, entries)
})
t.Run("merge_overlapping", func(t *testing.T) {
entries := Ranges{{
Offset: 2,
Length: 3,
}, {
Offset: 0,
Length: 3,
}}
entries.SortAndMerge()
require.Equal(t, Ranges{{
Offset: 0,
Length: 5,
}}, entries)
})
t.Run("merge_no_distance", func(t *testing.T) {
entries := Ranges{{
Offset: 2,
Length: 2,
}, {
Offset: 0,
Length: 2,
}}
entries.SortAndMerge()
require.Equal(t, Ranges{{
Offset: 0,
Length: 4,
}}, entries)
})
}

29
pkg/check/bounds.go Normal file
View File

@ -0,0 +1,29 @@
package check
import (
"github.com/9elements/converged-security-suite/v2/pkg/errors"
)
func bounds(length uint, startIdx, endIdx int) error {
result := &errors.MultiError{}
if startIdx < 0 {
result.Add(&errors.ErrStartLessThanZero{StartIdx: startIdx})
}
if endIdx < startIdx {
result.Add(&errors.ErrEndLessThanStart{StartIdx: startIdx, EndIdx: endIdx})
}
if endIdx >= 0 && uint(endIdx) > length {
result.Add(&errors.ErrEndGreaterThanLength{Length: length, EndIdx: endIdx})
}
return result.ReturnValue()
}
// BytesRange checks if starting index `startIdx`, ending index `endIdx` and
// len(b) passes sanity checks:
// * 0 <= startIdx
// * startIdx <= endIdx
// * endIdx < len(b)
func BytesRange(b []byte, startIdx, endIdx int) error {
return bounds(uint(len(b)), startIdx, endIdx)
}

36
pkg/errors/errors.go Normal file
View File

@ -0,0 +1,36 @@
package errors
import (
"fmt"
)
// ErrStartLessThanZero means `startIdx` has negative value
type ErrStartLessThanZero struct {
StartIdx int
}
func (err *ErrStartLessThanZero) Error() string {
return fmt.Sprintf("start index is less than zero: %d", err.StartIdx)
}
// ErrEndLessThanStart means `endIdx` value is less than `startIdx` value
type ErrEndLessThanStart struct {
StartIdx int
EndIdx int
}
func (err *ErrEndLessThanStart) Error() string {
return fmt.Sprintf("end index is less than start index: %d < %d",
err.EndIdx, err.StartIdx)
}
// ErrEndGreaterThanLength means `endIdx` is greater or equal to the length.
type ErrEndGreaterThanLength struct {
Length uint
EndIdx int
}
func (err *ErrEndGreaterThanLength) Error() string {
return fmt.Sprintf("end index is outside of the bounds: %d >= %d",
err.EndIdx, err.Length)
}

97
pkg/errors/multi_error.go Normal file
View File

@ -0,0 +1,97 @@
package errors
import (
"encoding/json"
"fmt"
"strings"
)
// MultiError is a type which aggregates multiple `error`-s into one `error`.
//
// Some functions may have multiple errors at one call, and sometimes
// we don't want to lose them (so we collect them all).
type MultiError []error
func (mErr MultiError) Error() string {
if mErr.Count() == 0 {
return "no errors"
}
var errStrings []string
for _, err := range mErr {
errStrings = append(errStrings, err.Error())
}
return fmt.Sprintf("errors: %v", strings.Join(errStrings, "; "))
}
// MarshalJSON just implements encoding/json.Marshaler
func (mErr MultiError) MarshalJSON() ([]byte, error) {
if len(mErr) == 0 {
return []byte("null"), nil
}
return json.Marshal(mErr.Error())
}
// Add adds errors to the collection.
//
// If some of passed errors are nil, then they won't be added
// (a `nil` error is not considered to be an error).
func (mErr *MultiError) Add(errs ...error) *MultiError {
for _, err := range errs {
if err == nil {
continue
}
switch err := err.(type) {
case *MultiError:
if err == nil {
continue
}
*mErr = append(*mErr, *err...)
case MultiError:
*mErr = append(*mErr, err...)
default:
*mErr = append(*mErr, err)
}
}
return mErr
}
// Count returns the amount of collected errors.
func (mErr MultiError) Count() uint {
return uint(len(mErr))
}
// ReturnValue is a helper which returns `nil` if there was
// no errors collected, and the collection if there're any.
//
// It supposed to be used in `return`-s:
// return mErr.ReturnValue()
func (mErr MultiError) ReturnValue() error {
if mErr.Count() > 0 {
return mErr
}
return nil
}
// Filter returns a copy of mErr but only with entries on which function "fn"
// returns "true".
func (mErr MultiError) Filter(fn func(err error) bool) MultiError {
var result MultiError
for _, err := range mErr {
if fn(err) {
result = append(result, err)
}
}
return result
}
// Unwrap is used by functions like `errors.As()`. Since it is not allowed
// to return multiple errors, it unwraps an error if there's only one.
func (mErr MultiError) Unwrap() error {
if mErr.Count() == 1 {
return mErr[0]
}
return nil
}

View File

@ -0,0 +1,19 @@
package errors
import (
"encoding/json"
"errors"
"testing"
"github.com/stretchr/testify/require"
)
func TestMultiErrorMarshalJSON(t *testing.T) {
mErr := MultiError{errors.New("simple error message"), errors.New(`I am a citizen of "Earth"!`)}
b, err := mErr.MarshalJSON()
require.NoError(t, err)
var errDescr string
err = json.Unmarshal(b, &errDescr)
require.NoError(t, err, string(b))
require.Equal(t, `"errors: simple error message; I am a citizen of \"Earth\"!"`, string(b))
}

37
pkg/mathtools/max_min.go Normal file
View File

@ -0,0 +1,37 @@
package mathtools
// Max returns a maximal value (from the `int`-s passed as the arguments)
func Max(arg0 int, args ...int) int {
result := arg0
for _, arg := range args {
if arg > result {
result = arg
}
}
return result
}
// UMax returns a maximal value (from the `uint`-s passed as the arguments)
func UMax(arg0 uint, args ...uint) uint {
result := arg0
for _, arg := range args {
if arg > result {
result = arg
}
}
return result
}
// Min returns a minimal value (from the `int`-s passed as the arguments)
func Min(arg0 int, args ...int) int {
result := arg0
for _, arg := range args {
if arg < result {
result = arg
}
}
return result
}

View File

@ -0,0 +1,37 @@
package ostools
import (
"fmt"
"io/ioutil"
"os"
"github.com/edsrzf/mmap-go"
)
// FileToBytes returns the contents of the file by path `filePath`.
func FileToBytes(filePath string) ([]byte, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf(`unable to open the image-file "%v": %w`,
filePath, err)
}
defer file.Close() // it was a read-only Open(), so we don't check the Close()
// To consume less memory we use mmap() instead of reading the image
// into the memory. However these bytes are also parsed by
// linuxboot/fiano/pkg/uefi which consumes a lot of memory anyway :(
//
// See "man 2 mmap".
contents, err := mmap.Map(file, mmap.RDONLY, 0)
if err == nil {
return contents, nil
}
// An error? OK, let's try the usual way to read data:
contents, err = ioutil.ReadAll(file)
if err != nil {
return nil, fmt.Errorf(`unable to access data of the image-file "%v": %w`,
filePath, err)
}
return contents, nil
}

68
pkg/tpmeventlog/errors.go Normal file
View File

@ -0,0 +1,68 @@
package tpmeventlog
import (
"fmt"
)
// ErrRead means unable to read from the io.Reader
type ErrRead struct {
Err error
}
// Error implements interface `error`.
func (err ErrRead) Error() string {
return fmt.Sprintf("unable to read the reader: %v", err.Err)
}
// Unwrap implements `xerrors.Wrapper`.
func (err ErrRead) Unwrap() error {
return err.Err
}
// ErrParse means unable to read from the io.Reader
type ErrParse struct {
Err error
}
// Error implements interface `error`.
func (err ErrParse) Error() string {
return fmt.Sprintf("unable to parse the EventLog: %v", err.Err)
}
// Unwrap implements `xerrors.Wrapper`.
func (err ErrParse) Unwrap() error {
return err.Err
}
// ErrLocality means it was unable to detect the locality to initialize
// the PCR0 value.
type ErrLocality struct {
EventData []byte
}
// Error implements interface `error`.
func (err ErrLocality) Error() string {
return fmt.Sprintf("unable to detect locality by event data '%x'", err.EventData)
}
// ErrInvalidDigestLength means an event has a digest of a size not appropriate
// for a selected hash algorithm.
type ErrInvalidDigestLength struct {
Expected int
Received int
}
// Error implements interface `error`.
func (err ErrInvalidDigestLength) Error() string {
return fmt.Sprintf("invalid digest length, expected:%d, received:%d", err.Expected, err.Received)
}
// ErrNotSupportedHashAlgo means selected hash algorithm is not supported (yet?)
type ErrNotSupportedHashAlgo struct {
TPMAlgo TPMAlgorithm
}
// Error implements interface `error`.
func (err ErrNotSupportedHashAlgo) Error() string {
return fmt.Sprintf("not supported hash algorithm: 0x%x", err.TPMAlgo)
}

View File

@ -0,0 +1,40 @@
package tpmeventlog
// EventType defines the kind of data reported by an Event.
//
// See also: https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientSpecPlat_TPM_2p0_1p04_pub.pdf#page=103
type EventType uint32
const (
EV_PREBOOT_CERT = EventType(0x00000000)
EV_POST_CODE = EventType(0x00000001)
EV_UNUSED = EventType(0x00000002)
EV_NO_ACTION = EventType(0x00000003)
EV_SEPARATOR = EventType(0x00000004)
EV_ACTION = EventType(0x00000005)
EV_EVENT_TAG = EventType(0x00000006)
EV_S_CRTM_CONTENTS = EventType(0x00000007)
EV_S_CRTM_VERSION = EventType(0x00000008)
EV_CPU_MICROCODE = EventType(0x00000009)
EV_PLATFORM_CONFIG_FLAGS = EventType(0x0000000A)
EV_TABLE_OF_DEVICES = EventType(0x0000000B)
EV_COMPACT_HASH = EventType(0x0000000C)
EV_IPL = EventType(0x0000000D)
EV_IPL_PARTITION_DATA = EventType(0x0000000E)
EV_NONHOST_CODE = EventType(0x0000000F)
EV_NONHOST_CONFIG = EventType(0x00000010)
EV_NONHOST_INFO = EventType(0x00000011)
EV_OMIT_BOOT_DEVICE_EVENTS = EventType(0x00000012)
EV_EFI_EVENT_BASE = EventType(0x80000000)
EV_EFI_VARIABLE_DRIVER_CONFIG = EventType(0x80000001)
EV_EFI_VARIABLE_BOOT = EventType(0x80000002)
EV_EFI_BOOT_SERVICES_APPLICATION = EventType(0x80000003)
EV_EFI_BOOT_SERVICES_DRIVER = EventType(0x80000004)
EV_EFI_RUNTIME_SERVICES_DRIVER = EventType(0x80000005)
EV_EFI_GPT_EVENT = EventType(0x80000006)
EV_EFI_ACTION = EventType(0x80000007)
EV_EFI_PLATFORM_FIRMWARE_BLOB = EventType(0x80000008)
EV_EFI_HANDOFF_TABLES = EventType(0x80000009)
EV_EFI_HCRTM_EVENT = EventType(0x80000010)
EV_EFI_VARIABLE_AUTHORITY = EventType(0x800000E0)
)

52
pkg/tpmeventlog/replay.go Normal file
View File

@ -0,0 +1,52 @@
package tpmeventlog
import (
"bytes"
pcr "github.com/9elements/converged-security-suite/v2/pkg/pcr/types"
)
// ParseLocality parses TPM locality from EV_NO_ACTION event corresponding
// to the TPM initialization.
func ParseLocality(eventData []byte) (uint8, error) {
// There no known way to reliably detect the locality using the event log,
// but here we will add working recipes for specific cases.
// event.Data example: "StartupLocality\x00\x03"
descrWords := bytes.Split(eventData, []byte{0})
switch {
case bytes.Compare(descrWords[0], []byte("StartupLocality")) == 0:
if len(descrWords) > 0 && len(descrWords[1]) == 1 {
return descrWords[1][0], nil
}
}
return 0, ErrLocality{EventData: eventData}
}
// FilterEvents returns only the events which has a specified PCR index and
// a digest of a specified hash algorithm.
func (eventLog *TPMEventLog) FilterEvents(pcrIndex pcr.ID, hashAlgo TPMAlgorithm) ([]*Event, error) {
hash, err := hashAlgo.Hash()
if err != nil {
return nil, ErrNotSupportedHashAlgo{TPMAlgo: hashAlgo}
}
hasher := hash.HashFunc()
var result []*Event
for _, event := range eventLog.Events {
if event.PCRIndex != pcrIndex {
continue
}
if event.Digest == nil || event.Digest.HashAlgo != hashAlgo {
continue
}
if len(event.Digest.Digest) != hasher.Size() {
return nil, ErrInvalidDigestLength{Expected: hasher.Size(), Received: len(event.Digest.Digest)}
}
result = append(result, event)
}
return result, nil
}

View File

@ -0,0 +1,72 @@
package tpmeventlog
import (
"io"
"io/ioutil"
"github.com/google/go-attestation/attest"
"github.com/google/go-tpm/tpm2"
pcr "github.com/9elements/converged-security-suite/v2/pkg/pcr/types"
)
// TPMEventLog is a parsed EventLog.
type TPMEventLog struct {
Events []*Event
}
// TPMAlgorithm is an identified of a TPM-supported hash algorithm.
//
// See also: https://trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf#page=42
type TPMAlgorithm = tpm2.Algorithm
// Event is a single entry of a parsed EventLog.
type Event struct {
PCRIndex pcr.ID
Type EventType
Data []byte
Digest *Digest
}
// Digest is the digest reported by an Event.
type Digest struct {
HashAlgo TPMAlgorithm
Digest []byte
}
const (
// TPMAlgorithmSHA1 is the identified of SHA1 algorithm.
TPMAlgorithmSHA1 = tpm2.AlgSHA1
// TPMAlgorithmSHA256 is the identified of SHA256 algorithm.
TPMAlgorithmSHA256 = tpm2.AlgSHA256
)
// Parse parses a binary EventLog.
func Parse(input io.Reader) (*TPMEventLog, error) {
b, err := ioutil.ReadAll(input)
if err != nil {
return nil, ErrRead{Err: err}
}
eventLog, err := attest.ParseEventLog(b)
if err != nil {
return nil, ErrParse{Err: err}
}
result := &TPMEventLog{}
for _, alg := range eventLog.Algs {
for _, entry := range eventLog.Events(alg) {
result.Events = append(result.Events, &Event{
PCRIndex: pcr.ID(entry.Index),
Type: EventType(entry.Type),
Data: entry.Data,
Digest: &Digest{
HashAlgo: TPMAlgorithm(alg),
Digest: entry.Digest,
},
})
}
}
return result, nil
}