constellation/internal/osimage/secureboot/secureboot.go

286 lines
8.0 KiB
Go

/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
// package secureboot holds secure boot configuration for image uploads.
package secureboot
import (
"bytes"
"compress/zlib"
"encoding/base64"
"encoding/binary"
"fmt"
"hash/crc32"
"io"
"os"
"github.com/spf13/afero"
)
// Database holds the secure boot database that cloud providers should
// use when enabling secure boot for a Constellation OS image.
type Database struct {
// PK is the platform key.
PK []byte
// Keks are trusted key-exchange-keys
Keks [][]byte
// DBs are entries of the signature database.
DBs [][]byte
}
// DatabaseFromFiles creates the secure boot database from individual files.
func DatabaseFromFiles(fs afero.Fs, pk string, keks []string, dbs []string) (Database, error) {
rawPK, err := afero.ReadFile(fs, pk)
if err != nil {
return Database{}, fmt.Errorf("loading PK %s: %w", pk, err)
}
rawKEKs := make([][]byte, len(keks))
for i, kek := range keks {
rawKEK, err := afero.ReadFile(fs, kek)
if err != nil {
return Database{}, fmt.Errorf("loading KEK %s: %w", kek, err)
}
rawKEKs[i] = rawKEK
}
rawDBs := make([][]byte, len(dbs))
for i, db := range dbs {
rawDB, err := afero.ReadFile(fs, db)
if err != nil {
return Database{}, fmt.Errorf("loading DB %s: %w", db, err)
}
rawDBs[i] = rawDB
}
return Database{
PK: rawPK,
Keks: rawKEKs,
DBs: rawDBs,
}, nil
}
// UEFIVarStore is a UEFI variable store.
// It is a collection of UEFIVar structs.
// This is an abstract var store that can convert to a concrete var store
// for a specific CSP.
type UEFIVarStore []UEFIVar
// VarStoreFromFiles creates the UEFI variable store
// from "EFI Signature List" (esl) files.
func VarStoreFromFiles(fs afero.Fs, pk, kek, db, dbx string) (UEFIVarStore, error) {
vars := UEFIVarStore{}
pkF, err := fs.OpenFile(pk, os.O_RDONLY, os.ModePerm)
if err != nil {
return UEFIVarStore{}, fmt.Errorf("opening PK ESL %s: %w", pk, err)
}
defer pkF.Close()
pkVar, err := ReadVar(pkF, "PK", globalEFIGUID)
if err != nil {
return UEFIVarStore{}, fmt.Errorf("reading PK ESL %s: %w", pk, err)
}
vars = append(vars, pkVar)
kekF, err := fs.OpenFile(kek, os.O_RDONLY, os.ModePerm)
if err != nil {
return UEFIVarStore{}, fmt.Errorf("opening KEK ESL %s: %w", kek, err)
}
defer kekF.Close()
kekVar, err := ReadVar(kekF, "KEK", globalEFIGUID)
if err != nil {
return UEFIVarStore{}, fmt.Errorf("reading KEK ESL %s: %w", kek, err)
}
vars = append(vars, kekVar)
dbF, err := fs.OpenFile(db, os.O_RDONLY, os.ModePerm)
if err != nil {
return UEFIVarStore{}, fmt.Errorf("opening DB ESL %s: %w", db, err)
}
defer dbF.Close()
dbVar, err := ReadVar(dbF, "db", secureDatabaseGUID)
if err != nil {
return UEFIVarStore{}, fmt.Errorf("reading DB ESL %s: %w", db, err)
}
vars = append(vars, dbVar)
if len(dbx) == 0 {
return vars, nil
}
dbxF, err := fs.OpenFile(dbx, os.O_RDONLY, os.ModePerm)
if err != nil {
return UEFIVarStore{}, fmt.Errorf("opening DBX ESL %s: %w", dbx, err)
}
defer dbxF.Close()
dbxVar, err := ReadVar(dbxF, "dbx", secureDatabaseGUID)
if err != nil {
return UEFIVarStore{}, fmt.Errorf("reading DBX ESL %s: %w", dbx, err)
}
vars = append(vars, dbxVar)
return vars, nil
}
// ToAWS converts the UEFI variable store to the AWS UEFI vars v0 format.
// The format is documented here:
// https://github.com/awslabs/python-uefivars
// It is structured as follows:
// Header:
// - 4 bytes: magic number
// - 4 bytes: crc32 of the rest of the file
// - 4 bytes: version number
//
// Body is zlib compressed stream of:
// 8 bytes number of entries
// for each entry:
// - name (variable length field, utf8)
// - data (variable length field)
// - guid (16 bytes)
// - attr (int32 in little endian)
// OPTIONAL (if attr has EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS set):
// - timestamp (16 bytes)
// - digest (variable length field).
func (s UEFIVarStore) ToAWS() (string, error) {
payload := bytes.Buffer{}
// Write the number of entries.
if err := binary.Write(&payload, binary.LittleEndian, uint64(len(s))); err != nil {
return "", fmt.Errorf("writing number of entries: %w", err)
}
// Write the entries.
for _, entry := range s {
rawEntry, err := entry.AWSEntry()
if err != nil {
return "", fmt.Errorf("serializing entry: %w", err)
}
if _, err := payload.Write(rawEntry); err != nil {
return "", fmt.Errorf("writing entry: %w", err)
}
}
// Compress the payload.
compressed := bytes.Buffer{}
zlibW, err := zlib.NewWriterLevelDict(&compressed, zlib.BestCompression, zlibDict)
if err != nil {
return "", fmt.Errorf("creating compressor: %w", err)
}
if _, err := zlibW.Write(payload.Bytes()); err != nil {
return "", fmt.Errorf("compressing payload: %w", err)
}
if err := zlibW.Close(); err != nil {
return "", fmt.Errorf("closing compressor: %w", err)
}
compressedData := compressed.Bytes()
// Calculate the CRC32 (Castagnoli) of the version + compressed payload.
crcData := append(awsVersion, compressedData...)
crc := crc32.Checksum(crcData, crc32.MakeTable(crc32.Castagnoli))
out := bytes.Buffer{}
// Write the header.
if _, err := out.Write(awsMagic); err != nil {
return "", fmt.Errorf("writing magic: %w", err)
}
if err := binary.Write(&out, binary.LittleEndian, crc); err != nil {
return "", fmt.Errorf("writing crc: %w", err)
}
// Write the version + compressed payload.
if _, err := out.Write(crcData); err != nil {
return "", fmt.Errorf("writing compressed payload: %w", err)
}
return base64.StdEncoding.EncodeToString(out.Bytes()), nil
}
// UEFIVar is a UEFI variable.
type UEFIVar struct {
Name string
Data []byte
GUID []byte
Attr uint32
Timestamp []byte
Digest []byte
}
// ReadVar reads a UEFI variable from an ESL file.
func ReadVar(reader io.Reader, name string, guid []byte) (UEFIVar, error) {
attr := uint32(
EFIVariableNonVolatile |
EFIVariableBootServiceAccess |
EFIVariableRuntimeAccess |
EFIVariableTimeBasedAuthenticatedWriteAccess,
)
data, err := io.ReadAll(reader)
if err != nil {
return UEFIVar{}, err
}
return UEFIVar{
Name: name,
Data: data,
GUID: guid,
Attr: attr,
Timestamp: []byte{
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
},
Digest: []byte{
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
},
}, nil
}
// AWSEntry returns the AWS format entry for the UEFI variable.
func (v UEFIVar) AWSEntry() ([]byte, error) {
var buf bytes.Buffer
if err := appendVariableLengthField(&buf, []byte(v.Name)); err != nil {
return nil, err
}
if err := appendVariableLengthField(&buf, v.Data); err != nil {
return nil, err
}
if _, err := buf.Write(v.GUID); err != nil {
return nil, err
}
if err := appendAttr(&buf, v.Attr); err != nil {
return nil, err
}
if v.Attr&EFIVariableTimeBasedAuthenticatedWriteAccess == 0 {
return buf.Bytes(), nil
}
if _, err := buf.Write(v.Timestamp); err != nil {
return nil, err
}
if err := appendVariableLengthField(&buf, v.Digest); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func appendVariableLengthField(w io.Writer, data []byte) error {
// variable length is encoded as unsigned, 64 bit little endian
// followed by the data
if err := binary.Write(w, binary.LittleEndian, uint64(len(data))); err != nil {
return err
}
_, err := w.Write(data)
return err
}
func appendAttr(w io.Writer, attr uint32) error {
return binary.Write(w, binary.LittleEndian, attr)
}
// EFI constants.
const (
EFIVariableNonVolatile = 0x00000001
EFIVariableBootServiceAccess = 0x00000002
EFIVariableRuntimeAccess = 0x00000004
EFIVariableTimeBasedAuthenticatedWriteAccess = 0x00000020
)
var (
awsMagic = []byte("AMZNUEFI")
awsVersion = []byte{0, 0, 0, 0}
globalEFIGUID = []byte{
0x61, 0xdf, 0xe4, 0x8b, 0xca, 0x93, 0xd2, 0x11,
0xaa, 0x0d, 0x00, 0xe0, 0x98, 0x03, 0x2b, 0x8c,
}
secureDatabaseGUID = []byte{
0xcb, 0xb2, 0x19, 0xd7, 0x3a, 0x3d, 0x96, 0x45,
0xa3, 0xbc, 0xda, 0xd0, 0x0e, 0x67, 0x65, 0x6f,
}
)