Intel BG/CBnT Metadata
April 30, 2026 · View on GitHub
This directory contains reusable definitions of common BG/CBnT structures and Boot Policy and Key Manifests for Intel platforms.
Structure
├── cbnt # Converged Boot Guard and TXT
│ ├── bootpolicy # BG/CBnT Boot Policy Manifest
│ ├── keymanifest # BG/CBnT Key Manifest
├── common # Common elements
│ ├── examples
│ ├── manifestcodegen # Legacy code generator
│ ├── pretty # Helper for printing the structures in human readable format
│ ├── tracedbinary
│ └── unittest #
└── fit # Firmware Interface Table
cbnt
The cbnt packages defines the structures that are used by both Boot Policy and Key Manifests.
Most of the structures are shared between Boot Guard 1.0 and CBnT, with an exception for the following:
- Chipset AC Module Information
- TPM Info List
bootpolicy
The bootpolicy package defines the Boot Policy Manifest and its child structures. In contrast with cbnt,
there are more differences between Boot Guard 1.0 and CBnT. Therefore, the Manifest constructor make the distinction
between the versions and returns the versioned implementation of the Manifest interface. Similar pattern is being used for
the structures that form the Manifest.
The users of the bootpolicy packages should therefore make use of type assertions. These are safe in this context provided
that the constructor is feed with the supported version, and the error is handled correctly. An example usage:
bpm, err := bootpolicy.NewManifest(cbnt.Version10)
if err != nil {
return nil, err
}
bgbpm = bpm.(*bootpolicy.ManifestBG)
From that point on, the elements of the implementation may be accessed directly, for example:
flags := bgbpm.SE[0].Flags
if !flags.AuthorityMeasure() {
return false, fmt.Errorf("pcr-7 data should extended for OS security")
}
if !flags.TPMFailureLeavesHierarchiesEnabled() {
return false, fmt.Errorf("tpm failure should lead to default measurements from PCR0 to PCR7")
}
keymanifest
The keymanifest package defines the Key Manifest. It follows the same design as bootpolicy. An example usage:
km, err := keymanifest.NewManifest(b.Version)
if err != nil {
return nil, err
}
cbntkm = km.(*keymanifest.CBnTManifest)
hash := cbntkm.PubKeyHashAlg
if hash == cbnt.AlgSHA1 || hash.IsNull() {
return false, fmt.Errorf("KM signature uses insecure hash algorithm SHA1/Null")
}
Structure Modification/Extension
All the structures should implement the Structure interface (see cbnt/types.go):
Structure interface {
io.ReaderFrom
io.WriterTo
TotalSize() uint64
SizeOf(id int) (uint64, error)
OffsetOf(id int) (uint64, error)
Layout() []LayoutField
Validate() error
PrettyString(depth uint, withHeader bool, opts ...pretty.Option) string
}
The most important method of the structure is the Layout() as it provides the Common.ReadFrom(), Common.WriteTo(), Common.SizeOf() and Common.OffsetOf() methods
with the information of the actual type the operation is supposed to be done. The common methods are accessed by letting all types to have Common struct
as a field.
Note
Common struct should never be included in the Layout()! Otherwise, it will be treated as the actual part of the CBnT data structure.
Types with Static Sizes
In most cases, that is, for the types that do not include fields that have their size determined at compile time, most of work is done in Layout()
method. Let's take StructureInfoCBNT as an example:
func (s StructInfoCBNT) Layout() []LayoutField {
return []LayoutField{
{
ID: 0,
Name: "ID",
Size: func() uint64 { return 8 },
Value: func() any { return &s.ID },
Type: ManifestFieldArrayStatic,
},
{
ID: 1,
Name: "Version",
Size: func() uint64 { return 1 },
Value: func() any { return &s.Version },
Type: ManifestFieldEndValue,
},
{
ID: 2,
Name: "Variable 0",
Size: func() uint64 { return 1 },
Value: func() any { return &s.Variable0 },
Type: ManifestFieldEndValue,
},
{
ID: 3,
Name: "Element Size",
Size: func() uint64 { return 2 },
Value: func() any { return &s.ElementSize },
Type: ManifestFieldEndValue,
},
}
}
All the values are known at compile time, and therefore the sizes defined in the specification can be filled in directly.
Types with Dynamic Sizes
The types that have dynamic sizes are different beasts. They should still describe all fields through Layout(), but the Size
closure is computed from runtime values. The actual implementation is left for the person introducing a new type.
There are three "Types" of dynamic sized fields:
ManifestFieldArrayDynamicWithSizefor byte arrays whose length is known from context (for example another field or algorithm-dependent size). An example can be seen inKeytype which is declared as:
type Key struct {
Common
KeyAlg Algorithm `json:"keyAlg"`
Version uint8 `require:"0x10" json:"keyVersion"`
KeySize BitSize `json:"keyBitsize"`
Data []byte `countValue:"keyDataSize()" json:"keyData"`
}
The size of Data field is equal to the value of KeySize which is first determined once the structure is filled-in on a call to Common.ReadFrom().
Therefore, the size closure in Key's Layout() has to return the value of KeySize:
{
ID: 3,
Name: "Data",
Size: func() uint64 { return uint64(k.keyDataSize()) },
Value: func() any { return &k.Data },
Type: ManifestFieldArrayDynamicWithSize,
},
ManifestFieldArrayDynamicWithPrefixfor byte arrays that carry their own size prefix on the wire. An example for such case is theHashStructuretype declared as:
type HashStructure struct {
Common
HashAlg Algorithm `default:"0x10" json:"hsAlg"`
HashBuffer []byte `json:"hsBuffer"`
}
The size of HashBuffer is dynamic, but unlike ManifestFieldArrayDynamicWithSize, this encoding stores a uint16
length prefix before the actual bytes. Therefore, the Size closure includes both the prefix and the payload (which depends on the hash algorithm type):
{
ID: 1,
Name: "Hash Buffer",
Size: func() uint64 {
h, err := s.HashAlg.Hash()
if err != nil {
return uint64(binary.Size(uint16(0)))
}
return uint64(binary.Size(uint16(0))) + uint64(h.Size())
},
Value: func() any { return &s.HashBuffer },
Type: ManifestFieldArrayDynamicWithPrefix,
},
Here, binary.Size(uint16(0)) is the on-wire size prefix.
ManifestFieldListfor repeated items where count and element parsing/writing are custom; this requiresReadListandWriteListhandlers. An example for such case is theHashListtype, declared as:
type HashList struct {
Common
Size uint16 `rehashValue:"TotalSize()" json:"hlSize"`
List []HashStructure `json:"hlList"`
}
Such types require addition fields to be returned in Layout(), and are an exception from the rule that all R/W logic is shared between the types. This is
motivated by the fact that Common.ReadFrom()/Common.WriteTo() cannot infer neither the type of the element on the list (well, it is possible with reflection,
though it would make common R/W methods look like dark magic), nor many elements should be processed and how each one should be
serialized. Therefore, ReadList and WriteList closures must be provided:
{
ReadList: func(r io.Reader) (int64, error) {
var count uint16
if err := binary.Read(r, endianess, &count); err != nil {
return 0, fmt.Errorf("unable to read the count for field 'List': %w", err)
}
totalN := int64(binary.Size(count))
s.List = make([]HashStructure, count)
for idx := range s.List {
n, err := s.List[idx].ReadFrom(r)
if err != nil {
return totalN, fmt.Errorf("unable to read field 'List[%d]': %w", idx, err)
}
totalN += int64(n)
}
return totalN, nil
},
WriteList: func(w io.Writer) (int64, error) {
count := uint16(len(s.List))
if err := binary.Write(w, binary.LittleEndian, &count); err != nil {
return 0, fmt.Errorf("unable to write the count for field 'List': %w", err)
}
totalN := int64(binary.Size(count))
for idx := range s.List {
n, err := s.List[idx].WriteTo(w)
if err != nil {
return totalN, fmt.Errorf("unable to write field 'List[%d]': %w", idx, err)
}
totalN += int64(n)
}
return totalN, nil
},
},
Common R/W method will then call the closures and let them do type specific work.
Similarly as with other dynamic sizes, the closure has to provide the way of the runtime calculation that can be done
after the call to Common.ReadFrom(). An example HashList:
{
Size: func() uint64 {
size := uint64(binary.Size(uint16(0)))
for idx := range s.List {
size += s.List[idx].Common.TotalSize(&s.List[idx])
}
return size
},
ManifestFieldSubStructfor fields that represent another type that implementStructureinterface. An example of such case is theKeySignaturetype declared as:
type KeySignature struct {
Common
Version uint8 `require:"0x10" json:"ksVersion,omitempty"`
Key Key `json:"ksKey"`
Signature Signature `json:"ksSignature"`
}
Effectively, in such case R/W and size/offsets of sub-types will be recursively determined using the common methods under the hood:
{
ID: 1,
Name: "Key",
Size: func() uint64 { return s.Key.Common.TotalSize(&s.Key) },
Value: func() any { return &s.Key },
Type: ManifestFieldSubStruct,
},
{
ID: 2,
Name: "Signature",
Size: func() uint64 { return s.Signature.Common.TotalSize(&s.Signature) },
Value: func() any { return &s.Signature },
Type: ManifestFieldSubStruct,
},
As implementing entity of a type that contains fields with sub-types, there is no need to concern about how sub-type will perform R/W and size/offset determination, as long it is known that its layout is correctly defined.
Note
for all dynamic fields, keep Size() aligned with the exact binary representation consumed/written by that field type.
Field Type Quick Reference
As described above, Common.ReadFrom() and Common.WriteTo() dispatch behavior by LayoutField.Type:
ManifestFieldEndValue: plain fixed-size scalar value.ManifestFieldArrayStatic: fixed-size array.ManifestFieldArrayDynamicWithSize: dynamic byte array with externally defined size.ManifestFieldArrayDynamicWithPrefix: dynamic byte array with encoded size prefix.ManifestFieldList: custom list logic viaReadList/WriteList.ManifestFieldSubStruct: nested structure implementingio.ReaderFrom/io.WriterTo.
Adding a Structure
This will be showcased on imaginary structure, let's call it X.
- Declare the struct (in the example we will use all
ManifestFieldTypevariants):
// Represents X structure as defined in document #nnnnnn
type X struct {
Common
UUID [16]byte `json:"superUUIDforXstruct"`
Version uint8 `require:"0x32" json:"versionX"`
Signature Signature `json:"signatureX"`
HashList []HashStructure `json:"hlList"`
SizeOfData1 BitSize `json:"szData1X"`
Data1 []byte `json:"data1X"`
Data2 []byte `json:"data2X"`
}
- Define
Layout()in strict on-wire order and map each field to aManifestFieldType:
func (s *X) Layout() []LayoutField {
return []LayoutField{
{
ID: 0,
Name: "UUID",
Size: func() uint64 { return 16 },
Value: func() any { return &s.UUID },
Type: ManifestFieldArrayStatic,
},
{
ID: 1,
Name: "Version",
Size: func() uint64 { return 1 },
Value: func() any { return &s.Version },
Type: ManifestFieldEndValue,
},
{
ID: 2,
Name: "Signature",
Size: func() uint64 { return s.Signature.Common.TotalSize(&s.Signature) },
Value: func() any { return &s.Signature },
Type: ManifestFieldSubStruct,
},
{
ID: 3,
Name: fmt.Sprintf("Hash List: length %d", len(s.HashList)),
Size: func() uint64 {
size := uint64(binary.Size(uint16(0)))
for idx := range s.HashList {
size += s.HashList[idx].TotalSize()
}
return size
},
Value: func() any { return &s.HashList },
Type: ManifestFieldList,
ReadList: func(r io.Reader) (int64, error) {
var count uint16
if err := binary.Read(r, endianess, &count); err != nil {
return 0, fmt.Errorf("unable to read count for field 'HashList': %w", err)
}
totalN := int64(binary.Size(count))
s.HashList = make([]HashStructure, count)
for idx := range s.HashList {
n, err := s.HashList[idx].ReadFrom(r)
if err != nil {
return totalN, fmt.Errorf("unable to read field 'HashList[%d]': %w", idx, err)
}
totalN += n
}
return totalN, nil
},
WriteList: func(w io.Writer) (int64, error) {
count := uint16(len(s.HashList))
if err := binary.Write(w, endianess, &count); err != nil {
return 0, fmt.Errorf("unable to write count for field 'HashList': %w", err)
}
totalN := int64(binary.Size(count))
for idx := range s.HashList {
n, err := s.HashList[idx].WriteTo(w)
if err != nil {
return totalN, fmt.Errorf("unable to write field 'HashList[%d]': %w", idx, err)
}
totalN += n
}
return totalN, nil
},
},
{
ID: 4,
Name: "SizeOfData1",
Size: func() uint64 { return 2 },
Value: func() any { return &s.SizeOfData1 },
Type: ManifestFieldEndValue,
},
{
ID: 5,
Name: "Data1",
Size: func() uint64 { return uint64(&s.SizeOfData1) },
Value: func() any { return &s.Data1 },
Type: ManifestFieldArrayDynamicWithSize,
},
{
ID: 6,
Name: "Data2",
Size: func() uint64 { return uint64(binary.Size(uint16(0)) + len(s.Data2)) },
Value: func() any { return &s.Data2 },
Type: ManifestFieldArrayDynamicWithPrefix,
},
}
}
- Keep common methods delegated to
Common:
func (s *X) ReadFrom(r io.Reader) (int64, error) { return s.Common.ReadFrom(r, s) }
func (s *X) WriteTo(w io.Writer) (int64, error) { return s.Common.WriteTo(w, s) }
func (s *X) TotalSize() uint64 { return s.Common.TotalSize(s) }
func (s *X) SizeOf(id int) (uint64, error) { return s.Common.SizeOf(s, id) }
func (s *X) OffsetOf(id int) (uint64, error) { return s.Common.OffsetOf(s, id) }
func (s *X) PrettyString(depth uint, withHeader bool, opts ...pretty.Option) string {
return s.Common.PrettyString(depth, withHeader, s, "X", opts...)
}
- Implement
Validate()and/orRehash()if any values are derived from other fields, for example:
func (s *X) Rehash() {
s.SizeOfData1 = BitSize(len(s.Data1))
}
func (s *X) Validate() error {
if int(s.SizeOfData1) != len(s.Data1) {
return fmt.Errorf("field 'SizeOfData1' expects %d, but has %d", len(s.Data1), s.SizeOfData1)
}
return nil
}
If you need a completely new field behavior beyond existing ManifestFieldType values,
extend the ManifestFieldType constants and add corresponding handling in both
Common.ReadFrom() and Common.WriteTo().
Extending a Structure
Let's take SECBnT as an example here, and assume that the update specification adds a field that stores the size of IBBSegments. Then we need to adapt the following:
- Type definition
type SECBnT struct {
cbnt.Common
cbnt.StructInfoCBNT `id:"__IBBS__" version:"0x20" var0:"0" var1:"uint16(s.TotalSize())"`
Reserved0 [1]byte `require:"0" json:"seReserved0,omitempty"`
SetNumber uint8 `require:"0" json:"seSetNumber,omitempty"`
Reserved1 [1]byte `require:"0" json:"seReserved1,omitempty"`
PBETValue PBETValue `json:"sePBETValue"`
Flags SEFlags `json:"seFlags"`
IBBMCHBAR uint64 `json:"seIBBMCHBAR"`
VTdBAR uint64 `json:"seVTdBAR"`
DMAProtBase0 uint32 `json:"seDMAProtBase0"`
DMAProtLimit0 uint32 `json:"seDMAProtLimit0"`
DMAProtBase1 uint64 `json:"seDMAProtBase1"`
DMAProtLimit1 uint64 `json:"seDMAProtLimit1"`
PostIBBHash cbnt.HashStructure `json:"sePostIBBHash"`
IBBEntryPoint uint32 `json:"seIBBEntry"`
DigestList cbnt.HashList `json:"seDigestList"`
OBBHash cbnt.HashStructure `json:"seOBBHash"`
Reserved2 [3]byte `require:"0" json:"seReserved2,omitempty"`
// NEW: size of IBBSegments
SizeOfIBBSeg [2]byte `json:"seSizeOfIBBSeg,omitemptu"`
IBBSegments []IBBSegment `countType:"uint8" json:"seIBBSegments,omitempty"`
}
- Layout descriptor
func (s *SECBnT) Layout() []cbnt.LayoutField {
return []cbnt.LayoutField{
{
ID: 0,
Name: "Struct Info",
Size: func() uint64 { return s.StructInfoCBNT.TotalSize() },
Value: func() any { return &s.StructInfoCBNT },
Type: cbnt.ManifestFieldSubStruct,
},
...
{
ID: 16,
Name: "Reserved 2",
Size: func() uint64 { return 3 },
Value: func() any { return &s.Reserved2 },
Type: cbnt.ManifestFieldArrayStatic,
},
// New entry
{
ID: 17,
Name: "Size of IBB Segments",
Size: func() uint64 { return 2 },
Value: func() any { return s.IBBSegments.TotalSize() }, // Yes, this example makes little sense, but it is more about the mechanics of the approach than logics of specification.
Type: cbnt.ManifestFieldArrayDynamicWithSize,
}
{
ID: 18, // Incremented
Name: fmt.Sprintf("IBBSegments: Array of \"IBB Segments Element\" of length %d", len(s.IBBSegments)),
Size: func() uint64 {
...
}
}
- Any affected API calls
SizeOf and OffsetOf methods depend on the ID of a field. Thus, after modifying the layout descriptor, these have to be adjusted.
Testing
There are two types of tests used for the metadata related packages:
- Unit Tests: for the structures in the
cbntpackages. - Integration Tests: for manifests. These are further described in a dedicated README.
To run all the tests:
go test ./cbnt