mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-10-01 01:36:09 -04:00
286 lines
8.0 KiB
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,
|
||
|
}
|
||
|
)
|