/
measurement.go
508 lines (439 loc) · 12.6 KB
/
measurement.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
package pcr
import (
"encoding/json"
"fmt"
"hash"
"reflect"
"github.com/9elements/converged-security-suite/v2/pkg/tpmeventlog"
"github.com/linuxboot/fiano/pkg/bytes"
)
var (
// LoggingDataLimit is a limit of how many bytes of a measured data
// to write per measurement. It is takes arbitrary (feel free to change it):
LoggingDataLimit = uint(20)
)
// DataChunk contains a chunk of data that is measured during one measurement.
// It could be a range of bytes inside firmware placed in `Range` or if `ForceData` is not nil,
// then it is used as the measurable data instead of `Range`.
type DataChunk struct {
ID DataChunkID `json:",omitempty"`
// Range contains byte range of the firmware to be measured
Range bytes.Range `json:",omitempty"`
// ForceData is used to define hard-coded values.
ForceData []byte `json:",omitempty"`
}
// String implements fmt.Stringer
func (chunk DataChunk) String() string {
m := map[string]string{}
m["ID"] = chunk.ID.String()
if chunk.ForceData != nil {
m["ForceData"] = fmt.Sprintf("0x%X", chunk.ForceData)
} else {
m["Offset"] = fmt.Sprintf("0x%X", chunk.Range.Offset)
m["Length"] = fmt.Sprintf("0x%X", chunk.Range.Length)
}
b, err := json.Marshal(m)
if err != nil {
panic(err)
}
return string(b)
}
// Find returns the chunk with the specified DataChunkID.
// Or returns nil if such data chunk was not found.
func (s DataChunks) Find(id DataChunkID) *DataChunk {
for idx := range s {
d := &s[idx]
if d.ID == id {
return d
}
}
return nil
}
// Copy performs a deep copy.
func (chunk DataChunk) Copy() *DataChunk {
if chunk.ForceData != nil {
forceData := make([]byte, len(chunk.ForceData))
copy(forceData, chunk.ForceData)
chunk.ForceData = forceData
}
return &chunk
}
// CompileMeasurableData returns the data to be measured.
func (chunk DataChunk) CompileMeasurableData(image []byte) []byte {
if chunk.ForceData != nil {
return chunk.ForceData
}
return image[chunk.Range.Offset : chunk.Range.Offset+chunk.Range.Length]
}
// DataChunks is a set of DataChunk-s.
type DataChunks []DataChunk
// Ranges returns a slice of all Range-s where ForceData is nil.
func (s DataChunks) Ranges() bytes.Ranges {
var r bytes.Ranges
for _, chunk := range s {
if chunk.ForceData != nil {
continue
}
r = append(r, chunk.Range)
}
return r
}
// Copy performs a deep copy.
func (s DataChunks) Copy() DataChunks {
if s == nil {
return nil
}
c := make(DataChunks, 0, len(s))
for _, data := range s {
c = append(c, *data.Copy())
}
return c
}
// NewStaticDataChunk returns a DataChunk based on a predefined value.
func NewStaticDataChunk(id DataChunkID, data []byte) *DataChunk {
return &DataChunk{
ID: id,
ForceData: data,
}
}
// NewRangeDataChunk returns a DataChunk based on image data.
func NewRangeDataChunk(id DataChunkID, offset uint64, length uint64) *DataChunk {
r := bytes.Range{
Offset: offset,
Length: length,
}
return &DataChunk{
ID: id,
Range: r,
}
}
// MeasureEvent describes a measurement event that can calculate the hash given the data
type MeasureEvent interface {
// NOTE: this would normally be named ID() but it conflicts with Measurement.ID that
// will implement this interface later on
GetID() MeasurementID
CompileMeasurableData(image []byte) []byte
Calculate(image []byte, hasher hash.Hash) ([]byte, error)
}
// Measurement is the key structure of all packages `pcr0/...`.
//
// It defines one PCR measurement. Usually it means to extend the
// PCR value with a hash of bytes referenced by `DataChunk`.
type Measurement struct {
// ID is the unique identifier of the PCR measurement.
ID MeasurementID
// Data contains chunks of data to be measured as contiguous sequence of bytes
Data DataChunks `json:",omitempty"`
}
func eventTypePtr(t tpmeventlog.EventType) *tpmeventlog.EventType {
return &t
}
// GetID return the measurement ID
func (m Measurement) GetID() MeasurementID {
return m.ID
}
// IsFake forces to skip this measurement in real PCR value calculation
func (m *Measurement) IsFake() bool {
if m == nil {
return true
}
return m.ID.IsFake()
}
// NoHash forces to skip hashing of this measurement's data during PCR calculation
func (m Measurement) NoHash() bool {
return m.ID.NoHash()
}
// EventLogEventTypes returns multiple potential values of
// "Type" field of the EventLog entry associated with the measurement.
func (m Measurement) EventLogEventTypes() []*tpmeventlog.EventType {
return m.ID.EventLogEventTypes()
}
func (m Measurement) String() string {
b, err := json.Marshal(&m)
if err != nil {
panic(err)
}
return string(b)
}
// Copy performs a deep copy.
func (m Measurement) Copy() *Measurement {
if m.Data != nil {
m.Data = m.Data.Copy()
}
return &m
}
// Equal performs a deep comparison of two measurements and returns true if
// they contain exactly the same information.
func (m *Measurement) Equal(cmp *Measurement) bool {
return reflect.DeepEqual(m, cmp)
}
// CompileMeasurableData returns all the bytes used for a PCR value measurement,
// referenced by `Data` from the image `uefi`.
func (m Measurement) CompileMeasurableData(image []byte) []byte {
if m.IsFake() {
return nil
}
var result []byte
for _, chunk := range m.Data {
fragment := chunk.CompileMeasurableData(image)
result = append(result, fragment...)
}
return result
}
// Ranges returns a slice of all Range-s where ForceData is nil.
func (m Measurement) Ranges() bytes.Ranges {
return m.Data.Ranges()
}
// Validate validates if this measurement could be safely applied to a given image.
func (m *Measurement) Validate(image []byte) error {
imgRange := bytes.Range{Offset: 0, Length: uint64(len(image))}
for _, r := range m.Ranges() {
outsideOfImageRange := r.Exclude(imgRange)
for _, r := range outsideOfImageRange { // TODO: fix in fiano to do not return empty ranges by Exclude
if r.Length > 0 {
return fmt.Errorf("ranges %#+v are outside of the provided image of size 0x%X", outsideOfImageRange, len(image))
}
}
}
return nil
}
// Calculate returns the hash from the gathered blocks from image
func (m *Measurement) Calculate(image []byte, hashFunc hash.Hash) ([]byte, error) {
if m.IsFake() {
return nil, nil
}
if err := m.Validate(image); err != nil {
return nil, fmt.Errorf("measurement %#+v is invalid: %w", *m, err)
}
data := m.CompileMeasurableData(image)
if m.NoHash() {
return data, nil
}
_, err := hashFunc.Write(data)
if err != nil {
return nil, err
}
defer hashFunc.Reset()
return hashFunc.Sum(nil), nil
}
// NewStaticDataMeasurement returns a measurement of a pre-defined value.
func NewStaticDataMeasurement(id MeasurementID, data []byte) *Measurement {
return &Measurement{
ID: id,
Data: []DataChunk{
{
ForceData: data,
},
},
}
}
// NewRangesMeasurement returns a measurement of multiple ranges of the
// firmware image
func NewRangesMeasurement(id MeasurementID, r bytes.Ranges) *Measurement {
chunks := make([]DataChunk, len(r))
for idx := range r {
chunks[idx].Range = r[idx]
}
return &Measurement{
ID: id,
Data: chunks,
}
}
// NewRangeMeasurement returns a measurement of a single range of a firmware
// image
func NewRangeMeasurement(id MeasurementID, offset uint64, length uint64) *Measurement {
r := bytes.Range{
Offset: offset,
Length: length,
}
return &Measurement{
ID: id,
Data: []DataChunk{
{
Range: r,
},
},
}
}
// CachedMeasurement is a Measurement with hash value computed at creation time
type CachedMeasurement struct {
Measurement
data []byte
hash []byte
}
func (m Measurement) Cache(image []byte, hasher hash.Hash) (*CachedMeasurement, error) {
hash, err := m.Calculate(image, hasher)
if err != nil {
return nil, err
}
data := m.CompileMeasurableData(image)
return &CachedMeasurement{m, data, hash}, nil
}
func (m CachedMeasurement) CompileMeasurableData(image []byte) []byte {
return m.data
}
func (m CachedMeasurement) Calculate(image []byte, hasher hash.Hash) ([]byte, error) {
return m.hash, nil
}
// Measurements is multiple Measurements.
// The order is important: PCR value will be calculated using the
// order this slice have (it won't be sorted in any way to do the calculation).
type Measurements []*Measurement
// AddOffset adds offset to all `Offset`-s of all ranges of all measurements.
//
// This could be used if the measurements are used against of a part of an
// UEFI image (instead of the whole image).
func (s Measurements) AddOffset(offset int64) {
for measurementIdx := range s {
if s[measurementIdx] == nil {
continue
}
for dataIdx := range s[measurementIdx].Data {
data := &s[measurementIdx].Data[dataIdx]
if data.Range.Length == 0 {
continue
}
adjustedOffset := int64(data.Range.Offset) + offset
if adjustedOffset < 0 {
panic(fmt.Errorf("negative offset: %d + %d -> %d", int64(data.Range.Offset), offset, adjustedOffset))
}
data.Range.Offset = uint64(adjustedOffset)
}
}
}
// Find returns the first measurement with the specified MeasurementID.
// Or returns nil, if such measurement was not found.
func (s Measurements) Find(id MeasurementID) *Measurement {
for _, measurement := range s {
if measurement.ID == id {
return measurement
}
}
return nil
}
// Copy performs a deep copy.
func (s Measurements) Copy() Measurements {
if s == nil {
return nil
}
c := make(Measurements, 0, len(s))
for _, m := range s {
c = append(c, m.Copy())
}
return c
}
// Data returns all the data chunks of all measurements
func (s Measurements) Data() DataChunks {
var result []DataChunk
for _, measurement := range s {
if measurement == nil {
continue
}
result = append(result, measurement.Data...)
}
return result
}
// Ranges returns a slice of all Range-s where ForceData is nil.
func (s Measurements) Ranges() bytes.Ranges {
return s.Data().Ranges()
}
// CompileMeasurableData returns all the bytes used for a PCR value measurement,
// references by all measurements of `s` from the image `uefi`.
func (s Measurements) CompileMeasurableData(image []byte) []byte {
var result []byte
for _, measurement := range s {
result = append(result, measurement.CompileMeasurableData(image)...)
}
return result
}
// FilterByPCRIndex returns a subset of measurements only which corresponds to specified PCR index.
func (s Measurements) FilterByPCRIndex(pcrIndex ID) Measurements {
var r Measurements
for _, m := range s {
found := false
for _, pcrIndexCmp := range m.ID.PCRIDs() {
if pcrIndexCmp == pcrIndex {
found = true
break
}
}
if !found {
continue
}
r = append(r, m)
}
return r
}
// FindOverlapping returns those measurements which overlaps with byte range
// `byteRange`.
func (s Measurements) FindOverlapping(byteRange bytes.Range) Measurements {
result := Measurements{}
for _, measurement := range s {
if measurement == nil {
continue
}
for _, chunk := range measurement.Data {
if byteRange.Intersect(chunk.Range) {
result = append(result, measurement)
break // breaks only the inner `for`
}
}
}
return result
}
// Printfer requires a method with signature of a standard Printf.
type Printfer interface {
Printf(fmt string, args ...interface{})
}
// Calculate [deprecated since 1jul2021] performs the calculation of the PCR value using image `uefi`.
func (s Measurements) Calculate(image []byte, initialValue uint8, hashFunc hash.Hash, logger Printfer) []byte {
mes := make([]MeasureEvent, len(s))
for i := range s {
mes[i] = s[i]
}
hash, err := CalculatePCR(image, initialValue, mes, hashFunc, logger)
if err != nil {
panic(err)
}
return hash
}
// CalculatePCR performs the calculation of the PCR value using image `uefi`.
func CalculatePCR(image []byte, initialValue uint8, measureEvents []MeasureEvent, hasher hash.Hash, logger Printfer) ([]byte, error) {
result := make([]byte, hasher.Size())
result[len(result)-1] = initialValue
if logger != nil {
logger.Printf("Set 0x -> 0x%X\n\n", result)
}
for _, m := range measureEvents {
hash, err := m.Calculate(image, hasher)
if err != nil {
return nil, err
}
if hash == nil {
continue
}
if logger != nil {
data := m.CompileMeasurableData(image)
if uint(len(data)) > LoggingDataLimit {
logger.Printf("Event '%s': %x... (len: %d) (%T)\n", m.GetID(), data[:LoggingDataLimit], len(data), hasher)
} else {
logger.Printf("Event '%s': %x (%T)\n", m.GetID(), data, hasher)
}
}
_, err = hasher.Write(result)
if err != nil {
return nil, err
}
_, err = hasher.Write(hash)
if err != nil {
return nil, err
}
oldResult := result
result = hasher.Sum(nil)
hasher.Reset()
if logger != nil {
logger.Printf("%T(0x %X %X) == 0x%X\n\n", hasher, oldResult, hash, result)
}
}
return result, nil
}