mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-05-02 14:26:23 -04:00
image: implement idempotent upload of os images
This commit is contained in:
parent
17c45bc881
commit
ee91d8b1cc
42 changed files with 4272 additions and 95 deletions
285
internal/osimage/secureboot/secureboot.go
Normal file
285
internal/osimage/secureboot/secureboot.go
Normal file
|
@ -0,0 +1,285 @@
|
|||
/*
|
||||
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,
|
||||
}
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue