Рекомендации по настройке доверенной загрузки ядра

Содержание:

Введение

1. Подготовка программного обеспечения

2. Построение цепочки доверия в UEFI Secure Boot
2.1. Создание ключей и генерация сертификатов
2.2. Конвертация сертификатов в формат EFI Signature List
2.3. Формирование базы разрешенных ключей db
2.4. Формирование базы отозванных ключей dbx
2.5. Подпись цепочки сертификатов
2.6. Создание загрузочной флешки с KeyTool

3. Создание Unified Kernel Image и отказ от стороннего загрузчика
3.1. Создание файла аргументов ядра Linux
3.2. Подготовка initramfs. Встраивание микрокода процессора
3.3. Создание EFI-образа Unified Kernel Image
3.3.1. В производных от Debian дистрибутивах
3.3.2. В производных от Arch Linux дистрибутивах
3.3.3. В дистрибутиве Alt Linux
3.4. Подпись созданного Unified Kernel Image
3.5. Автоматизация генерации Unified Kernel Image
3.6. Добавление Unified Kernel Image в загрузку

4. Подпись ядра, модулей ядра, функция Kernel Lockdown
4.1. Подпись ядра Linux
4.2. Подпись модулей ядра Linux
4.3. Kernel Lockdown

5. Конфигурация UEFI Secure Boot и установка ключей
5.1. Настройка UEFI Secure Boot в средах виртуализации
5.1.1 VMWare Fusion / VMWare Workstation
5.1.2 QEMU + OVMF
5.1.3 VirtualBox
5.2. Установка сертификатов с помощью KeyTool

Заключение

Введение

Этот документ рассматривает настройку доверенной загрузки преимущественно для рабочих станций с архитектурой x86, x86_64 и не затрагивает вопросы централизованного управления, но при этом не противоречит им. Централизованное управление, в частности, развертывание ключей Secure Boot может быть организовано с помощью специального программного обеспечения разработчиками дистрибутивов или иными средствами, применяемыми системными администраторами на местах.

анные рекомендации призваны обезопасить процесс запуска дистрибутивов семейства Linux. Основная концепция доверенной загрузки заключается в том, что управление операционной системе должно быть передано только в том случае, если механизмы доверенной загрузки (МДЗ) платформы могут криптографически проверить загрузочные компоненты на соответствие предварительно сконфигурированному набору открытых ключей, подписей и хэшей, а также ряд других параметров.

Средства доверенной загрузки (СДЗ) подразделяются на следующие типы:

  • средства доверенной загрузки уровня базовой системы ввода-вывода (СДЗ уровня БСВВ);
  • средства доверенной загрузки уровня платы расширения (СДЗ уровня ПР);
  • средства доверенной загрузки уровня загрузочной записи (СДЗ уровня ЗЗ).

Важно отметить, что стандартный механизм UEFI Secure Boot не может быть сертифицирован в качестве СДЗ, но при этом на концептуальном уровне он соответствует СДЗ уровня БСВВ.

Популярные дистрибутивы, в том числе и отечественные, поддерживающие реализацию доверенной загрузки через механизм UEFI Secure Boot и по умолчанию содержат ряд недостатков:

  1. Список доверенного ПО, порождаемый сертификатами Microsoft и Canonical через shim, является практически бесконечным и бесконтрольным даже для тех дистрибутивов, загрузчику которых доверяет shim.
  2. Отзыв уязвимых версий загрузчика происходит медленно и влечёт за собой либо отзыв загрузчиков всех дистрибутивов, либо приводит к значительному росту размера чёрного списка сертификатов и хэшей (dbx), который хранится в NVRAM и уже сейчас составляет более 14 КБ.
  3. Ключ владельца аппаратуры (MOK), также используемый shim для аутентификации загрузчиков UEFI и модулей ядра Linux может быть изменён на произвольный любым пользователем с правами root, который обладает физическим доступом к компьютеру.
  4. Аргументы загрузки ядра хранятся на изменяемой файловой системе (в конфигурации загрузчика), в NVRAM переменной загрузочной записи (Boot####) или редактируются прямо из интерфейса загрузчика, позволяя получить root-доступ.
  5. Цепочка доверия заканчивается на модулях ядра и не проверяет ни файловую систему начальной загрузки (initrd), ни какие-либо пользовательские программы.
  6. Постоянно выявляемые уязвимости в загрузчике операционных систем GRUB2 делают его ненадежным для использования в конфигурации Secure Boot, что компрометирует всю цепочку доверия, равно как и использование любого уязвимого промежуточного звена в виде загрузчиков операционных систем, которые перехватывают функции BootServices.

Количество потенциальных угроз можно было бы значительно сократить за счёт:

  • использования отдельного CA каждым дистрибутивом, устанавливаемого разработчиком аппаратной платформы или системным администратором;
  • использования подписанного Unified Kernel Image (UKI) вместо grub2 и initrd;
  • отказа от MOK и возможности влиять на список загружаемых модулей ядра.

если реализовать следующую схему загрузки ОС Linux:

LVCSecureBoot 1

Использование СДЗ уровня ПР также возможно в предложенной схеме. Преимуществом использования UKI является возможность сократить список контролируемых файлов с помощью СДЗ до одного файла (UKI-образа) и гарантировать проверку целостности компонентов родным для Linux-ядра способом.

Тем не менее при использовании СДЗ уровня ПР возникает ряд нюансов, в частности это проблема совместимости платы расширения конкретного СДЗ с UEFI SecureBoot, а также проблема организации доверенного хранилища сертификатов, которое формируется ядром Linux из ключей платформы.

С учетом вышеупомянутых проблем, для создания замкнутой программной среды (ЗПС) с СДЗ уровня ПР требуется реализация одного из перечисленных ниже подходов:

  1. UEFI Secure Boot не поддерживается платой расширения СДЗ, ключ платформы можно хранить в переменной MOK-ключа, которая должна быть установлена СДЗ на стадии загрузки до передачи управления ядру Linux в виде volatile переменной во избежание возможности её дальнейшей перезаписи. MOK-ключ так же как и переменные SecureBoot платформы загружается в хранилище ключей .platform.
  2. UEFI Secure Boot должен быть включён и совместим с платой расширения СДЗ. Т.е. СДЗ уровня ПР должен аутентифицировать ключи UEFI Secure Boot до их использования прошивкой.
  3. Все сертификаты должны быть интегрированы в ядро на стадии сборки, а загрузка внешних сертификатов должна быть запрещена.

Для защиты от TOCTOU атак проверка UKI-образа должна быть произведена СДЗ уровня ПР только в оперативной памяти и последующей загрузкой его без повторного чтения с носителя.

Основной задачей руководства продемонстрировать представленную схему загрузки, а также дать рекомендации по обеспечению цепочки доверия от старта встроенного программного обеспечения стандарта UEFI до пользовательского окружения операционной системы семейства Linux.

1. Подготовка программного обеспечения

Для настройки UEFI Secure Boot необходимо установить дополнительное программное обеспечение, а именно:

  1. Пакет openssl, одноименная утилита из которого используется для генерации ключевых пар и преобразования сертификатов.
  2. Пакет efitools, который предоставляет утилиты cert-to-efi-sig-list, sign-efi-sig-list для преобразования сертификатов в формат ESL[1] и осуществления подписи файлов в этом формате; утилиту hash-to-efi-sig-list для создания ESL из хешей отдельных исполняемых файлов, а также KeyTool.efi для управления ключами системы, находящихся в SetupMode.
  3. Пакет sbsigntool, из которого используется утилита sbsign для подписи исполняемых EFI-файлов с помощью вашего ключа.
  4. Пакет efibootmgr, который содержит одноименное приложение для управления загрузочными записями UEFI.
  5. Пакет uuid-runtime, который содержит утилиты для работы с UUID; необходима утилита uuidgen.
  6. Пакет binutils, который предоставляет программы для ассемблирования, компоновки и манипуляций с двоичными и объектными файлами. Из этого пакета будут использованы утилиты objcopy и objdump.
  7. Пакет linux-headers-$(uname -r), который предоставляет заголовки ядра Linux. $(uname -r) указывает версию запущенного ядра в настоящий момент. Заголовки являются частью ядра, хотя и поставляются отдельно; они действуют как интерфейс между внутренними компонентами ядра, а также между пользовательским пространством и ядром. В их комплекте поставляются утилиты для операций по модификации ядра vmlinuz (перепаковка, распаковка), а также инструмент для подписи модулей ядра sign-file.

Обработка двоичных файлов UEFI SecureBoot (PK/KEK/db/dbx/dbxupdate) будет осуществляться скриптом Python.


#!/usr/bin/env python3
from dataclasses import dataclass, field
import enum
import uuid

class Bytes(bytes):
    def __str__(self):
        return self.hex()

    def __repr__(self):
        return self.hex()

class Enum(enum.Enum):
    def __str__(self):
        return self.name

    def __repr__(self):
        return self.name

def u16(data):
    assert len(data) == 2
    return int.from_bytes(data, byteorder='little')

def u32(data):
    assert len(data) == 4
    return int.from_bytes(data, byteorder='little')

class EFI_SIGNATURE_TYPE(Enum):
    EFI_CERT_SHA256_GUID         = uuid.UUID('c1c41626-504c-4092-aca9-41f936934328')
    EFI_CERT_RSA2048_GUID        = uuid.UUID('3c5766e8-269c-4e34-aa14-ed776e85b3b6')
    EFI_CERT_RSA2048_SHA256_GUID = uuid.UUID('e2b36190-879b-4a3d-ad8d-f2e7bba32784')
    EFI_CERT_SHA1_GUID           = uuid.UUID('826ca512-cf10-4ac9-b187-be01496631bd')
    EFI_CERT_RSA2048_SHA1_GUID   = uuid.UUID('67f8444f-8743-48f1-a328-1eaab8736080')
    EFI_CERT_X509_GUID           = uuid.UUID('a5c059a1-94e4-4aa7-87b5-ab155c2bf072')

class EFI_SIGNATURE_OWNER(Enum):
    EFI_SIGNATURE_OWNER_CANONICAL = uuid.UUID('685984e3-5d0f-4682-94c1-0f85ecb55d34')
    EFI_SIGNATURE_OWNER_MICROSOFT = uuid.UUID('77fa9abd-0359-4d32-bd60-28f4e78f784b')

@dataclass
class EFI_SIGNATURE_DATA:
    Owner: uuid.UUID
    Data: Bytes

    @classmethod
    def from_bytes(cls, data):
        return cls(
            Owner=EFI_SIGNATURE_OWNER(uuid.UUID(bytes_le=data[:0x10])),
            Data=Bytes(data[0x10:]),
        )

@dataclass
class EFI_SIGNATURE_LIST(list):
    Type: EFI_SIGNATURE_TYPE
    Size: int
    Header: Bytes

    @classmethod
    def from_bytes(cls, data):
        Type = EFI_SIGNATURE_TYPE(uuid.UUID(bytes_le=data[:0x10]))
        ListSize = u32(data[0x10:0x14])
        assert len(data) >= ListSize
        HeaderSize = u32(data[0x14:0x18])
        SignatureSize = u32(data[0x18:0x1C])
        assert SignatureSize > 0
        offsetof_Signatures = 0x1C + HeaderSize
        sizeof_Signatures = ListSize - offsetof_Signatures
        assert sizeof_Signatures % SignatureSize == 0
        Signatures = data[offsetof_Signatures:ListSize]
        self = cls(
            Type=Type,
            Size=ListSize,
            Header=Bytes(data[0x1C:0x1C+HeaderSize]),
        )
        self += [
            EFI_SIGNATURE_DATA.from_bytes(Signatures[offset:offset+SignatureSize])
            for offset in range(0, sizeof_Signatures, SignatureSize)
        ]
        return self

    def __repr__(self):
        Signatures = '\n'.join(f' {item},' for item in self)
        return f'EFI_SIGNATURE_LIST(Type={self.Type}, Header={self.Header}, Signatures=[\n{Signatures}\n])'

@dataclass
class EFI_TIME:
    Year: int
    Month: int
    Day: int
    Hour: int
    Minute: int
    Second: int
    Nanosecond: int
    TimeZone: int
    Daylight: int

    @classmethod
    def from_bytes(cls, data):
        assert len(data) == 16
        Pad1 = data[7]
        Pad2 = data[15]
        assert Pad1 == Pad2 == 0
        return cls(
            Year=u16(data[0:2]),
            Month=data[2],
            Day=data[3],
            Hour=data[4],
            Minute=data[5],
            Second=data[6],
            Nanosecond=u32(data[8:12]),
            TimeZone=u16(data[12:14]),
            Daylight=data[14],
        )


class WIN_CERT_TYPE(Enum):
    WIN_CERT_TYPE_PKCS_SIGNED_DATA = 0x0002
    WIN_CERT_TYPE_EFI_PKCS115      = 0x0EF0
    WIN_CERT_TYPE_EFI_GUID         = 0x0EF1

@dataclass(init=False)
class WIN_CERTIFICATE:
    Length: int
    Revision: int
    CertificateType: WIN_CERT_TYPE
    Certificate: Bytes

    @classmethod
    def from_bytes(cls, data):
        self = cls()
        self.Length = u32(data[0:4])
        assert self.Length > 8
        self.Revision = u16(data[4:6])
        assert self.Revision == 0x0200, f'Revision=0x{self.Revision:X}'
        self.CertificateType = WIN_CERT_TYPE(u16(data[6:8]))
        self.Certificate = data[8:self.Length]
        return self

    def __len__(self):
        return self.Length

class EFI_CERT_TYPE(Enum):
    EFI_CERT_TYPE_PKCS7_GUID          = uuid.UUID('4aafd29d-68df-49ee-8aa9-347d375665a7')
    EFI_CERT_TYPE_RSA2048_SHA256_GUID = uuid.UUID('a7717414-c616-4977-9420-844712a735bf')

@dataclass(init=False)
class WIN_CERTIFICATE_UEFI_GUID(WIN_CERTIFICATE):
    Certificate: Bytes = field(repr=False)
    CertType: EFI_CERT_TYPE
    CertData: Bytes

    @classmethod
    def from_bytes(cls, data):
        self = super().from_bytes(data)
        self.CertType = EFI_CERT_TYPE(uuid.UUID(bytes_le=self.Certificate[:16]))
        self.CertData = Bytes(self.Certificate[16:])
        return self

@dataclass
class EFI_VARIABLE_AUTHENTICATION_2:
    TimeStamp: EFI_TIME
    AuthInfo: WIN_CERTIFICATE_UEFI_GUID

    @classmethod
    def from_bytes(cls, data):
        return cls(
            TimeStamp=EFI_TIME.from_bytes(data[:16]),
            AuthInfo=WIN_CERTIFICATE_UEFI_GUID.from_bytes(data[16:]),
        )

    def __len__(self):
        return 0x10 + len(self.AuthInfo)

    def __repr__(self):
        return f'EFI_VARIABLE_AUTHENTICATION_2(\n TimeStamp={self.TimeStamp},\n AuthInfo={self.AuthInfo},\n)'

def parse_binary(data):
    if data[:4] == b'\x27\x00\x00\x00':
        # skip 4 bytes attributes exposed by efivarfs filesystem
        data = data[4:]
    if data[22:24] != b'\x00\x00':
        var_auth_2 = EFI_VARIABLE_AUTHENTICATION_2.from_bytes(data)
        print(var_auth_2)
        data = data[len(var_auth_2):]
    offset = 0
    while offset < len(data):
        sig_list = EFI_SIGNATURE_LIST.from_bytes(data[offset:])
        print(sig_list)
        offset += sig_list.Size

def parse_file(path):
    import mmap
    with open(path, 'rb') as f:
        try:
            data = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
        except OSError:
            data = f.read()
    parse_binary(data)

def main():
    import argparse
    parser = argparse.ArgumentParser('Secure Boot related blob parser (for PK/KEK/db/dbx/dbxupdate)')
    parser.add_argument('path', help='path to input blob')
    args = parser.parse_args()
    parse_file(args.path)

if __name__ == '__main__':
    main()

Команды для установки программного обеспечения в различных дистрибутивах:

sudo apt update
sudo apt install openssl efitools efibootmgr sbsigntool uuid-runtime linux-headers-$(uname -r) binutils keyutils

Для Astra Linux дополнительно необходимо установить пакет astra-safepolicy, более подробно в официальной документации.

sudo apt install astra-safepolicy

Стоит отметить, что в указанной статье используется скрипт astra-secureboot, который выполняет большую часть шагов этого руководства.

sudo pacman -Sy
sudo pacman -S openssl efitools sbsigntools efibootmgr util-linux linux-headers binutils keyutils

Команды, исполняемые с правами суперпользователя, в данной статье вызываются с помощью утилиты sudo. По умолчанию в дистрибутиве Alt Linux "Рабочая станция" команда sudo выключена. Для того чтобы включить команду sudo, необходимо отредактировать файл /etc/sudoers, раскомментировав строки root ALL=(ALL) ALL и WHEEL_USERS ALL=(ALL) ALL из учетной записи суперпользователя, используя команду visudo:

su -
visudo
sudo apt-get update
sudo apt-get install openssl efitools sbsigntools binutils util-linux kernel-headers-std-def kernel-headers-un-def efibootmgr keyutils

1 EFI Signature List - формат хранения файлов сертификатов и подписей, описанный в спецификации UEFI (в п. 32.4.1 вер. 2.10). Основной особенностью является возможность соединять файлы друг с другом конкатенацией (прим. cat bootmgfw.efi.esl bootx64.efi.esl > final_siglist.esl).

2. Построение цепочки доверия в UEFI Secure Boot

Сегодня в большинстве конфигураций UEFI Secure Boot платформы по умолчанию главный ключ (PK - Platform Key) предоставляется производителем материнской платы, а в хранилище ключей (KEK - Key Exchange Key) лежит единственный сертификат Microsoft Corporation KEK CA 2011, хотя иногда к нему добавляют сертификат Canonical и сертификат производителя платформы. Помимо этого, в хранилище есть также база разрешенных ключей (db - Signature Database Key) и база отозванных ключей (dbx - Forbidden Signature Database).

LVCSecureBoot 2

Здесь PK – это главный ключ, которым подписан KEK, в свою очередь ключами из KEK подписываются db и dbx. Для запуска подписанного исполняемого файла, он должен быть подписан ключом, который есть в db и отсутствует в dbx. Первым шагом на пути к построению цепочки доверия является создание сертификатов и приватных ключей.

2.1 Создание ключей и генерация сертификатов

Приватные ключи, созданные в процессе данной процедуры, важно хранить в защищенном хранилище. Утечка приватного ключа означает компрометацию всей цепочки доверия и, как следствие, всей защищаемой платформы. Рекомендуется использовать для каждого ключа отдельный, криптостойкий пароль.

С помощью приведенных ниже команд выполняется создание ключевых пар UEFI Secure Boot с использованием алгоритма RSA2048/SHA256.

Далее необходимо выполнить преобразование открытых ключей в формат UEFI Secure Boot – EFI Signature List.

2.2 Преобразование открытых ключей в формат EFI Signature List

Ранее созданные сертификаты необходимо преобразовать из контейнера PEM в формат ESL с помощью утилиты cert-to-efi-sig-list, как показано ниже.

owner=$(uuidgen owner=$(uuidgen --namespace @dns --name mega_secured_pc.local --sha1))
echo -n $owner > owner_uuid.txt
cert-to-efi-sig-list -g $owner PK.pem PK.esl
cert-to-efi-sig-list -g $owner KEK.pem KEK.esl
cert-to-efi-sig-list -g $owner ISK.pem ISK.esl

LVCSecureBoot 6

Здесь в качестве аргумента -g передается случайный идентификатор, сгенерированный с помощью утилиты uuidgen. Данный UUID идентифицирует владельца ключа. Обратите внимание, что в этих командах у всех трех ключей владелец (owner) один. Тем не менее, эта опция полезна для дифференциации ключей в системе, когда их много, если присваивать каждому ключу свой уникальны идентификатор. По умолчанию, если не указать свой UUID при преобразовнии, используется пустой (00000000-0000-0000-0000-000000000000).

Следующим этапом выполняется создание базы разрешенных ключей db.

2.3 Формирование базы разрешенных ключей db

На этом этапе необходимо принять решение, добавлять ли ключи подписи Microsoft в доверенные (в db) к ранее созданному ISK.esl или не добавлять.

Чтобы добавить возможность запуска подписанных загрузчиков операционных систем Windows, достаточно добавить ключи Microsoft Windows Production CA 2011[2]. Для поддержки драйверов различного оборудования необходимо также добавить ключ Microsoft UEFI driver signing CA[3], которым подписываются компоненты третьих сторон.

Для этого выполняется преобразование сертификата Microsoft Windows Production CA 2011 из формата DER в совместимый с утилитой cert-to-efi-sig-list формат PEM:

openssl x509 -in MicWinProPCA2011_2011-10-19.crt -inform DER -out MicWinProPCA2011_2011-10-19.pem -outform PEM

Аналогичные действия производятся для Microsoft UEFI driver signing CA:

openssl x509 -in MicCorUEFCA2011_2011-06-27.crt -inform DER -out MicCorUEFCA2011_2011-06-27.pem -outform PEM

Далее выполняется преобразование полученных сертификатов из контейнера PEM в ESL:

cert-to-efi-sig-list -g "$(uuidgen)" MicWinProPCA2011_2011-10-19.pem MicWinProPCA2011_2011-10-19.esl
cert-to-efi-sig-list -g "$(uuidgen)" MicCorUEFCA2011_2011-06-27.pem MicCorUEFCA2011_2011-06-27.esl

После этого создается база разрешенных ключей db:

cat ISK.esl > db.esl

cat MicWinProPCA2011_2011-10-19.esl ISK.esl > db.esl

cat MicWinProPCA2011_2011-10-19.esl MicCorUEFCA2011_2011-06-27.esl ISK.esl > db.esl

Альтернативным путем является добавление не сертификатов Microsoft, а доверенных хешей исполняемых EFI-файлов, а именно - файлов загрузчика Windows. Как показала практика, исполняемые файлы загрузчика Windows изменяются крайне редко, поэтому регулярно обновлять базу разрешенных хэш-значений не придётся.

Для этого необходимо преобразовать SHA-256 от исполняемых файлов загрузчика Windows в ESL формат:

  • EFI/BOOT/bootx64.efi
  • EFI/Microsoft/Boot/bootmgfw.efi
  • EFI/Microsoft/Boot/bootmgr.efi
  • EFI/Microsoft/Boot/memtest.efi
hash-to-efi-sig-list EFI/BOOT/bootx64.efi ms_1.esl
hash-to-efi-sig-list EFI/Microsoft/Boot/bootmgfw.efi ms_2.esl
hash-to-efi-sig-list EFI/Microsoft/Boot/bootmgr.efi ms_3.esl
hash-to-efi-sig-list EFI/Microsoft/Boot/memtest.efi ms_4.esl

LVCSecureBoot 7

В результате, полученные ESL-файлы добавляются в базу разрешенных ключей db.esl командой:

cat ms_1.esl ms_2.esl ms_3.esl ms_4.esl ISK.esl > db.esl

В таком случае необходимо добавить хеши всех исполняемых файлов, в том числе драйверов периферийных устройств, GOP и т.д Также вы можете добавлять любой хеш приложения или драйвера, какой пожелаете в качестве доверенного.

Не добавляйте UEFI Shell в качестве доверенного, не подписывайте его и приложения со схожим функционалом, поскольку это может позволить обойти механизм UEFI Secure Boot. В частности, не подписывайте и не доверяйте загрузчику GRUB. Также не подписывайте KeyTool!

2 http://go.microsoft.com/fwlink/?LinkID=321192
3 http://go.microsoft.com/fwlink/?LinkId=321194

Следующий шаг - это создание базы отозванных ключей dbx. Он опционален, поскольку dbx необходим только в том случае, если вы добавили сторонние ключи Microsoft или Canonical в базу разрешенных ключей db или в KEK.

2.4 Формирование базы отозванных ключей dbx

Принцип создания базы отозванных ключей dbx не отличается от принципа создания db. Файлы обновления для базы отозванных ключей публикуются на сайте UEFI консорциума по ссылке. Чтобы заполнить базу отозванных ключей из файла обновления, предварительно выполняется его преобразование в формат ESL.

Файл обновления содержит структуру EFI_VARIABLE_AUTHENTICATION_2, в которой располагается электронная подпись Microsoft KEK, а также сам список отозванных хешей в формате ESL. Необходимо удалить заголовок с подписью для преобразования файла обновления dbx в формат ESL. Для этого с помощью sb_blob_parser определяется размер структуры EFI_VARIABLE_AUTHENTICATION_2, содержащей электронную подпись:

python3 ~/sb_blob_parser.py DBXUpdate.bin

LVCSecureBoot 8

К полученному значению Length добавляется еще 16 байт (размер EFI_TIME Timestamp) и результирующим значением в этом примере будет: 3318+16 = 3334.

Затем с помощью утилиты dd отрезается часть файла обновления, содержащая EFI Signature List командой:

dd if=DBXUpdate.bin of=dbx.esl bs=1 skip=3334

Для проверки корректности полученного dbx.esl, можно загрузить его в sb_blob_parser:

python3 sb_blob_parser.py dbx.esl

LVCSecureBoot 9

Когда все компоненты готовы (PK, KEK, db, dbx), можно перейти к их подписи.

2.5 Подпись цепочки сертификатов

На данном этапе выполняется подпись сертификатов по цепочке PK->KEK->db,dbx.

PK подписывается самим собой командой:

sign-efi-sig-list -k PK.key -c PK.pem PK PK.esl PK.auth

Сертификатом PK подписывается KEK командой:

sign-efi-sig-list -k PK.key -c PK.pem KEK KEK.esl KEK.auth

Сертификатом KEK подписывается база разрешенных ключей db командой:

sign-efi-sig-list -k KEK.key -c KEK.pem db db.esl db.auth

Если был сформирован dbx, то он подписывается также сертификатом KEK:

sign-efi-sig-list -k KEK.key -c KEK.pem dbx dbx.esl dbx.auth

Следующим шагом необходимо создать загрузочный накопитель с утилитой KeyTool и сгенерированными ключами UEFI Secure Boot (*.auth).

2.6 Создание загрузочной флешки с KeyTool

Флеш-накопитель необходимо отформатировать в файловую систему FAT-32. После чего скопировать утилиту KeyTool.efi на флешку по пути /EFI/BOOT/bootx64.efi. Утилита KeyTool поставляется в составе пакета efitools. На системах семейства Debian она расположена по пути /usr/lib/efitools/x86_64-linux-gnu/KeyTool.efi, а в Arch Linux – /usr/share/efitools/efi/KeyTool.efi.

Файлы .auth также необходимо скопировать на созданный раздел загрузочного накопителя.

LVCSecureBoot 10

3. Создание Unified Kernel Image и отказ от стороннего загрузчика

При использовании загрузчика GRUB, ему предоставляется файл конфигурации, initramfs, и образ системы vmlinuz, содержащий ядро Linux. Файл initramfs включает в себя базовый набор компонентов дистрибутива Linux, отвечающих за расшифровку разделов, монтирование файловой системы и других разделов с последующим запуском подсистемы инициализации init.

Идеей является вложить все эти составляющие в один исполняемый файл. Для этого существует несколько способов:

  • использовать заготовку EFI Stub из пакета systemd для создания PE-исполняемого файла с помощью утилиты objcopy;
  • скомпилировать ядро самостоятельно с включенной опцией ядра CONFIG_EFI_STUB=y, встроить initramfs и аргументы ядра.

Нами рекомендуется первый способ, ввиду того, что даже в случае если вам понадобиться скомпилировать ядро самостоятельно, такой подход позволяет без особых трудностей обновлять секции Unified Kernel Image, в частности, содержимое initramfs и значения аргументов ядра (root=UUID..., --verbose, ro, rw, quite и пр.).

Создаваемые образы, следуя systemd спецификации загрузчика, размещаются по пути /EFI/Linux/ на разделе EFI System Partition.

3.1 Создание файла с аргументами загрузки ядра Linux

Перед созданием Unified Kernel Image необходимо подготовить файл с аргументами загрузки ядра. Текущие аргументы ядра, которые использовались при загрузке системы, можно посмотреть по пути /proc/cmdline.

В качестве примера на следующем рисунке представлены стандартные аргументы, передаваемые ядру ОС Debian 11 во время загрузки:

LVCSecureBoot 11

Здесь аргумент root= указывает ядру, какое устройство должно быть использовано в качестве корневой файловой системы, параметр ro указывает ядру монтировать корневую файловую систему в режиме «только для чтения», для того чтобы программа проверки целостности файловой системы (fsck) могла работать с неизменяющейся файловой системой. Аргумент BOOT_IMAGE= необходимо отбросить, потому что vmlinuz будет расположен в секции .linux Unified Kernel Image. Более подробную информацию об аргументах ядра Linux можно получить в официальном руководстве администратора – https://www.kernel.org/doc/html/<версия ядра>/admin-guide/kernel-parameters.html, где версия ядра может иметь значение latest для mainline ветки или цифровое обозначение, например:

Таким образом, создается файл со всеми требуемыми аргументами загрузки ядра по пути /etc/kernel/cmdline:

sudo bash -c "cat /proc/cmdline | tr -d '\n' | sed 's/.*root=/root=/g' > /etc/kernel/cmdline"

Либо вручную, как показано в примере команды ниже:

sudo bash -c "echo -n 'root=UUID=cc9e7638-ca8e-47d2-bab1-13e4e61037da ro quiet' > /etc/kernel/cmdline"

3.2 Подготовка initramfs. Встраивание микрокода процессора

Немаловажным моментом является встраивание микрокода в initramfs.

В дистрибутивах, производных от Debian, в том числе Astra Linux, необходимо установить пакет intel-microcode для процессоров семейства Intel или amd64-microcode для процессоров AMD соответственно. Установленный пакет с микрокодом автоматически добавит hook в утилиту генерации initramfs, который будет добавлять необходимый файл обновления микрокода в initrd при каждом обновлении.

После установки необходимого пакета с микрокодом требуется выполнить его конфигурацию. Для amd64-microcode надо раскомментировать строку AMD64UCODE_INITRAMFS и присвоить ей значение early в файле /etc/default/amd64-microcode, что соответствует раннему запуску обновления микрокода процессора (very early microcode updates).

LVCSecureBoot 12

А для intel-microcode в файле /etc/default/intel-microcode выполнить такие же действия для переменной IUCODE_TOOL_INITRAMFS.

LVCSecureBoot 13

После этого выполняется обновление initramfs командой:

sudo update-initramfs -u -k all

Более подробно про микрокод в Debian рассказывется в официальной Wiki

Также обратите внимание, что файлы микрокода на Debian расположены по путям:

/usr/lib/firmware/amd-ucode/
/usr/lib/firmware/intel-ucode/

Для дистрибутива Alt Linux требуемый программный пакет - firmware-linux. По умолчанию initrd генерируется с микрокодом процессора платформы и дополнительных действий не требуется, достаточно обновить initrd командой:

sudo make-initrd --kernel=$(uname -r)

Больше информации про утилиту make-initrd в Alt Linux можно найти в официальной Wiki и в репозитории одноименного проекта на Github.

Для дистрибутивов, производных от Arch Linux (Manjaro, Garuda и пр.), необходимо "склеить" микрокод с initramfs с помощью команды cat:
cat /boot/intel-ucode.img /boot/initramfs-linux.img > /boot/initramfs-linux-with-microcode.img

Командой выше показан пример с микрокодом Intel, но, в общем, нейминг выполняется в следующей форме: производитель_цп-ucode.img, т.е. для AMD будет amd-ucode.img.

Файлы *-ucode.img в /boot могут отсутствовать, в этом случае необходимо установить пакеты amd-ucode, intel-ucode с помощью пакетного менеджера pacman командами:

sudo pacman -Sy
sudo pacman -S amd-ucode
или
sudo pacman -S intel-ucode

Дополнительную информацию про настройку обновлений микрокода можно найти в официальной Wiki Arch Linux.

PS: Хотелось еще упомянуть неофициальный, но постоянно обновляемый, репозиторий с микрокодами для разных CPU - https://github.com/platomav/CPUMicrocodes.

На этом манипуляции с initramfs можно считать завершенными и перейти к созданию Unified Kernel Image с помощью утилиты objcopy.

3.3 Создание EFI-образа Unified Kernel Image

На данном этапе в EFI-заготовку из пакета systemd в качестве дополнительных секций будут встроены необходимые загрузочные файлы с помощью утилиты objcopy из пакета binutils. Обязательными секциями являются .osrel и .cmdline. В этом руководстве также добавляется ядро и рамдиск, что в дальнейших шагах позволит подписать все компоненты, требуемые для загрузки ядра, как единое целое.

Доступные секции для добавления в файл-заготовку:

  • .osrel, в ней указывается информация о дистрибутиве, как правило содержимое расположено по пути /etc/os-release (в некоторых дистрибутивах в /usr/lib/os-release).
  • .cmdline, в этой секции хранятся аргументы ядра. В случае если ядро не добавляется в качестве секции в EFI-образ, то необходимо указать его расположение в качестве аргумента в этой секции. Необходимые аргументы можно взять из /proc/cmdline, но вы можете записать свои собственные в файл и использовать их вместо этого. Рекомендуется хранить аргументы ядра по пути /etc/kernel/cmdline.
  • .linux, в данной секции хранится образ системы vmlinuz или vmlinux, содержащий ядро Linux.
  • .initrd, а в эту секцию записывается initramfs.
  • .dtb, в эту секцию можно записать свой DeviceTree, который заменит текущий в EFI configuration table.

Более подробно с systemd-stub можно ознакомиться на ресурсе freedesktop.

Важно заметить, что на платформах с архитектурой ARM64 существует две проблемы в дистрибутивах Linux. Первая связана с тем, что на большинстве LTS-дистрибутивах используется довольно старый пакет binutils, и это обязательно вызовет ошибку при использовании утилит objdump и objcopy на устройствах с этой архитектурой из-за бага, который исправили только в декабре 2021 года. К сожалению, пакеты в таких дистрибутивах как Debian обновляются очень медленно, поэтому возможным решением для них будет аналог утилиты objcopy, реализованный, например, на python, либо самостоятельная компиляция пакета binutils свежей версии из исходного кода, который можно скачать с официального сайта в виде тарбола. А вторая проблема заключается в отсутствии декомпрессора в Linux ядре для архитектуры aarch64, что ломает загрузку сжатых образов системы vmlinuz с использованием EFI-заготовки из состава systemd. Поэтому предварительно следует распаковать ядро (vmlinux), как временное решение (ref).

Также, обратите внимание, что в приведенных командах ниже создаются обезличенные UKI-образы с названием файла linux.efi или linux-fallback.efi, что может не соответствовать политике по именованию ядер Linux в вашем конкретном дистрибутиве. Для того чтобы иметь возможность загрузить конкретную версию ядра, вы можете именовать ваши UKI-образы более информативно соответствующим образом - linux-kernel_version.efi, прим. linux-5.10.0-15-amd64.efi. Для этого вы можете указывать в качестве сохраняемого файла имя - $(uname -r).efi, что будет соответствовать текущей версии загруженного ядра Linux. В таком случае, имейте в виду, что для каждого такого ядра требуется создавать свою загрузочную запись в EFI Boot Manager (см. п.п. 3.6)

3.3.1 В производных от Debian дистрибутивах

Для производных от Debian дистрибутивов, в том числе Astra Linux, команды следующие:

sudo mkdir -p /boot/efi/EFI/Linux

efistub_path=$(case $(uname -m) in "x86_64") echo "/usr/lib/systemd/boot/efi/linuxx64.efi.stub";; \
"i686") echo "/usr/lib/systemd/boot/efi/linuxia32.efi.stub";; \
"aarch64") echo "/usr/lib/systemd/boot/efi/linuxaa64.efi.stub";; \
*) echo "unsupported arch" $(uname -m);; esac;)
vmlinuz_path=$(sudo realpath /vmlinuz)
initrd_path=$(sudo realpath /initrd.img)
osrelease_path=$(realpath /etc/os-release)
stub_line=$(objdump -h $efistub_path | tail -2 | head -1)
stub_size=0x$(echo "$stub_line" | awk '{print $3}')
stub_offs=0x$(echo "$stub_line" | awk '{print $4}')
osrel_offs=$((stub_size + stub_offs))
cmdline_offs=$((osrel_offs + $(stat -c%s $osrelease_path)))
linux_offs=$((cmdline_offs + $(stat -c%s "/etc/kernel/cmdline")))
initrd_offs=$((linux_offs + $(sudo stat -c%s "$vmlinuz_path")))

sudo objcopy \
    --add-section .osrel=$osrelease_path \
    --change-section-vma .osrel=$(printf 0x%x $osrel_offs) \
    --add-section .cmdline="/etc/kernel/cmdline" \
    --change-section-vma .cmdline=$(printf 0x%x $cmdline_offs) \
    --add-section .linux=$vmlinuz_path \
    --change-section-vma .linux=$(printf 0x%x $linux_offs) \
    --add-section .initrd=$initrd_path \
    --change-section-vma .initrd=$(printf 0x%x $initrd_offs) \
    $efistub_path "/boot/efi/EFI/Linux/linux.efi"

Предполагается, что initrd уже содержит файл-обновление микрокода процессора (т.е. ранее был установлен программный пакет, соответствующий процессору аппаратной платформы: amd64-microcode, intel-microcode). В этих командах используются символьные ссылки, расположенные в корне файловой системы, которые ссылаются на последнюю версию ядра (vmlinuz) и рамдиска (initrd.img) в системной папке /boot.

Для того чтобы была возможность загрузиться с прошлой версии ядра, можно создать резервный EFI-образ для /vmlinuz.old и /initrd.img.old:

sudo mkdir -p /boot/efi/EFI/Linux

efistub_path=$(case $(uname -m) in "x86_64") echo "/usr/lib/systemd/boot/efi/linuxx64.efi.stub";; \
"i686") echo "/usr/lib/systemd/boot/efi/linuxia32.efi.stub";; \
"aarch64") echo "/usr/lib/systemd/boot/efi/linuxaa64.efi.stub";; \
*) echo "unsupported arch" $(uname -m);; esac;)
vmlinuz_path=$(sudo realpath /vmlinuz.old)
initrd_path=$(sudo realpath /initrd.img.old)
osrelease_path=$(realpath /etc/os-release)
stub_line=$(objdump -h $efistub_path | tail -2 | head -1)
stub_size=0x$(echo "$stub_line" | awk '{print $3}')
stub_offs=0x$(echo "$stub_line" | awk '{print $4}')
osrel_offs=$((stub_size + stub_offs))
cmdline_offs=$((osrel_offs + $(stat -c%s $osrelease_path)))
linux_offs=$((cmdline_offs + $(stat -c%s "/etc/kernel/cmdline")))
initrd_offs=$((linux_offs + $(sudo stat -c%s "$vmlinuz_path")))

sudo objcopy \
    --add-section .osrel=$osrelease_path \
    --change-section-vma .osrel=$(printf 0x%x $osrel_offs) \
    --add-section .cmdline="/etc/kernel/cmdline" \
    --change-section-vma .cmdline=$(printf 0x%x $cmdline_offs) \
    --add-section .linux=$vmlinuz_path \
    --change-section-vma .linux=$(printf 0x%x $linux_offs) \
    --add-section .initrd=$initrd_path \
    --change-section-vma .initrd=$(printf 0x%x $initrd_offs) \
    $efistub_path "/boot/efi/EFI/Linux/linux-fallback.efi"

Подписанный резервный EFI-образ дает возможность загрузки прошлой версии ядра, что негативно может сказаться на безопасности системы. Тем не менее, это существенно повышает надежность платформы, предоставляя возможность отката в случае неуспешного обновления основного ядра.

3.3.2 В производных от Arch Linux дистрибутивах

Для дистрибутива Arch Linux и производных от него ОС (например: Manjaro Linux, Garuda Linux и пр.) команды принимают следующий вид:

sudo mkdir -p /boot/efi/EFI/Linux

efistub_path=$(case $(uname -m) in "x86_64") echo "/usr/lib/systemd/boot/efi/linuxx64.efi.stub";; \
"i686") echo "/usr/lib/systemd/boot/efi/linuxia32.efi.stub";; \
"aarch64") echo "/usr/lib/systemd/boot/efi/linuxaa64.efi.stub";; \
*) echo "unsupported arch" $(uname -m);; esac;)
vmlinuz_path="/boot/vmlinuz-linux"
initrd_path="/boot/initramfs-linux.img"
osrelease_path=$(realpath /etc/os-release)
stub_line=$(objdump -h $efistub_path | tail -2 | head -1)
stub_size=0x$(echo "$stub_line" | awk '{print $3}')
stub_offs=0x$(echo "$stub_line" | awk '{print $4}')
osrel_offs=$((stub_size + stub_offs))
cmdline_offs=$((osrel_offs + $(stat -c%s $osrelease_path)))
linux_offs=$((cmdline_offs + $(stat -c%s "/etc/kernel/cmdline")))
initrd_offs=$((linux_offs + $(sudo stat -c%s "$vmlinuz_path")))

sudo objcopy \
    --add-section .osrel=$osrelease_path \
    --change-section-vma .osrel=$(printf 0x%x $osrel_offs) \
    --add-section .cmdline="/etc/kernel/cmdline" \
    --change-section-vma .cmdline=$(printf 0x%x $cmdline_offs) \
    --add-section .linux=$vmlinuz_path \
    --change-section-vma .linux=$(printf 0x%x $linux_offs) \
    --add-section .initrd=$initrd_path \
    --change-section-vma .initrd=$(printf 0x%x $initrd_offs) \
    $efistub_path "/boot/efi/EFI/Linux/linux.efi"

Названия файлов ядра и рам-диска (переменные vmlinuz_path, initrd_path) в вашей системе могут отличаться от представленных выше. Например, если вы выполняли склейку initramfs с файлом обновления микрокода, то необходимо указать initrd_path в /boot/initramfs-linux-with-microcode.img.

Так же как и в дистрибутиве Debian, вы можете создать резервный EFI-образ, только в данном случае выбирается не предыдущее установленное ядро, а ядро с длительным сроком поддержки (LTS):

sudo mkdir -p /boot/efi/EFI/Linux

efistub_path=$(case $(uname -m) in "x86_64") echo "/usr/lib/systemd/boot/efi/linuxx64.efi.stub";; \
"i686") echo "/usr/lib/systemd/boot/efi/linuxia32.efi.stub";; \
"aarch64") echo "/usr/lib/systemd/boot/efi/linuxaa64.efi.stub";; \
*) echo "unsupported arch" $(uname -m);; esac;)
vmlinuz_path="/boot/vmlinuz-linux-lts"
initrd_path="/boot/initramfs-linux-lts-fallback.img"
osrelease_path=$(realpath /etc/os-release)
stub_line=$(objdump -h $efistub_path | tail -2 | head -1)
stub_size=0x$(echo "$stub_line" | awk '{print $3}')
stub_offs=0x$(echo "$stub_line" | awk '{print $4}')
osrel_offs=$((stub_size + stub_offs))
cmdline_offs=$((osrel_offs + $(stat -c%s $osrelease_path)))
linux_offs=$((cmdline_offs + $(stat -c%s "/etc/kernel/cmdline")))
initrd_offs=$((linux_offs + $(sudo stat -c%s "$vmlinuz_path")))

sudo objcopy \
    --add-section .osrel=$osrelease_path \
    --change-section-vma .osrel=$(printf 0x%x $osrel_offs) \
    --add-section .cmdline="/etc/kernel/cmdline" \
    --change-section-vma .cmdline=$(printf 0x%x $cmdline_offs) \
    --add-section .linux=$vmlinuz_path \
    --change-section-vma .linux=$(printf 0x%x $linux_offs) \
    --add-section .initrd=$initrd_path \
    --change-section-vma .initrd=$(printf 0x%x $initrd_offs) \
    $efistub_path "/boot/efi/EFI/Linux/linux-fallback.efi"

Названия файлов ядра и рам-диска (переменные vmlinuz_path, initrd_path) на вашей системе могут отличаться от представленных выше!

LVCSecureBoot 14

В рамках этой статьи данная опция будет рассмотрена только для Arch Linux, ввиду того, что дистрибутивы, основанные на этой ОС, имеют предустановленный файл с изображением на этапе загрузки в составе systemd, но это не означает, что эта опция недоступна на других дистрибутивах, просто для них необходимо создавать картинку самостоятельно, или брать из тем загрузчика GRUB или Plymouth.

Команды для создания Unified Kernel Image с изображением на этапе загрузки:

sudo mkdir -p /boot/efi/EFI/Linux

efistub_path=$(case $(uname -m) in "x86_64") echo "/usr/lib/systemd/boot/efi/linuxx64.efi.stub";; \
"i686") echo "/usr/lib/systemd/boot/efi/linuxia32.efi.stub";; \
"aarch64") echo "/usr/lib/systemd/boot/efi/linuxaa64.efi.stub";; \
*) echo "unsupported arch" $(uname -m);; esac;)
vmlinuz_path="/boot/vmlinuz-linux"
initrd_path="/boot/initramfs-linux.img"
osrelease_path=$(realpath /etc/os-release)
stub_line=$(objdump -h $efistub_path | tail -2 | head -1)
stub_size=0x$(echo "$stub_line" | awk '{print $3}')
stub_offs=0x$(echo "$stub_line" | awk '{print $4}')
osrel_offs=$((stub_size + stub_offs))
cmdline_offs=$((osrel_offs + $(stat -c%s $osrelease_path)))
splash_offs=$((cmdline_offs + $(stat -c%s "/etc/kernel/cmdline")))
linux_offs=$((splash_offs + $(stat -c%s "/usr/share/systemd/bootctl/splash-arch.bmp")))
initrd_offs=$((linux_offs + $(sudo stat -c%s "/boot/vmlinuz-linux")))

sudo objcopy \
    --add-section .osrel=$osrelease_path \
    --change-section-vma .osrel=$(printf 0x%x $osrel_offs) \
    --add-section .cmdline="/etc/kernel/cmdline" \
    --change-section-vma .cmdline=$(printf 0x%x $cmdline_offs) \
    --add-section .splash="/usr/share/systemd/bootctl/splash-arch.bmp" \
    --change-section-vma .splash=$(printf 0x%x $splash_offs) \
    --add-section .linux=$vmlinuz_path \
    --change-section-vma .linux=$(printf 0x%x $linux_offs) \
    --add-section .initrd=$initrd_path \
    --change-section-vma .initrd=$(printf 0x%x $initrd_offs) \
    $efistub_path "/boot/efi/EFI/Linux/linux.efi"

Команды для создания резервного EFI-образа:

sudo mkdir -p /boot/efi/EFI/Linux

efistub_path=$(case $(uname -m) in "x86_64") echo "/usr/lib/systemd/boot/efi/linuxx64.efi.stub";; \
"i686") echo "/usr/lib/systemd/boot/efi/linuxia32.efi.stub";; \
"aarch64") echo "/usr/lib/systemd/boot/efi/linuxaa64.efi.stub";; \
*) echo "unsupported arch" $(uname -m);; esac;)
vmlinuz_path="/boot/vmlinuz-linux-lts"
initrd_path="/boot/initramfs-linux-lts-fallback.img"
osrelease_path=$(realpath /etc/os-release)
stub_line=$(objdump -h $efistub_path | tail -2 | head -1)
stub_size=0x$(echo "$stub_line" | awk '{print $3}')
stub_offs=0x$(echo "$stub_line" | awk '{print $4}')
osrel_offs=$((stub_size + stub_offs))
cmdline_offs=$((osrel_offs + $(stat -c%s $osrelease_path)))
splash_offs=$((cmdline_offs + $(stat -c%s "/etc/kernel/cmdline")))
linux_offs=$((splash_offs + $(stat -c%s "/usr/share/systemd/bootctl/splash-arch.bmp")))
initrd_offs=$((linux_offs + $(sudo stat -c%s "$vmlinuz_path")))

sudo objcopy \
    --add-section .osrel=$osrelease_path \
    --change-section-vma .osrel=$(printf 0x%x $osrel_offs) \
    --add-section .cmdline="/etc/kernel/cmdline" \
    --change-section-vma .cmdline=$(printf 0x%x $cmdline_offs) \
    --add-section .splash="/usr/share/systemd/bootctl/splash-arch.bmp" \
    --change-section-vma .splash=$(printf 0x%x $splash_offs) \
    --add-section .linux=$vmlinuz_path \
    --change-section-vma .linux=$(printf 0x%x $linux_offs) \
    --add-section .initrd=$initrd_path \
    --change-section-vma .initrd=$(printf 0x%x $initrd_offs) \
    $efistub_path "/boot/efi/EFI/Linux/linux-fallback.efi"
3.3.3 В дистрибутиве Alt Linux

В дистрибутиве Alt Linux именование бинарных пакетов с Linux образами отличается от других дистрибутивов. Схема именования Linux ядер описывается в официальной Wiki и там же про их разновидности. В рамках данного руководства рассматривается создание EFI-образа для стандартного, основного (production) ядра std-def:

sudo mkdir -p /boot/efi/EFI/Linux

efistub_path=$(case $(uname -m) in "x86_64") echo "/usr/lib/systemd/boot/efi/linuxx64.efi.stub";; \
"i686") echo "/usr/lib/systemd/boot/efi/linuxia32.efi.stub";; \
"aarch64") echo "/usr/lib/systemd/boot/efi/linuxaa64.efi.stub";; \
*) echo "unsupported arch" $(uname -m);; esac;)
vmlinuz_path=$(sudo realpath /boot/vmlinuz-std-def)
initrd_path=$(sudo realpath /boot/initrd-std-def.img)
osrelease_path=$(realpath /etc/os-release)
stub_line=$(objdump -h $efistub_path | tail -2 | head -1)
stub_size=0x$(echo "$stub_line" | awk '{print $3}')
stub_offs=0x$(echo "$stub_line" | awk '{print $4}')
osrel_offs=$((stub_size + stub_offs))
cmdline_offs=$((osrel_offs + $(stat -c%s $osrelease_path)))
linux_offs=$((cmdline_offs + $(stat -c%s "/etc/kernel/cmdline")))
initrd_offs=$((linux_offs + $(sudo stat -c%s "$vmlinuz_path")))

sudo objcopy \
    --add-section .osrel=$osrelease_path \
    --change-section-vma .osrel=$(printf 0x%x $osrel_offs) \
    --add-section .cmdline="/etc/kernel/cmdline" \
    --change-section-vma .cmdline=$(printf 0x%x $cmdline_offs) \
    --add-section .linux=$vmlinuz_path \
    --change-section-vma .linux=$(printf 0x%x $linux_offs) \
    --add-section .initrd=$initrd_path \
    --change-section-vma .initrd=$(printf 0x%x $initrd_offs) \
    $efistub_path "/boot/efi/EFI/Linux/linux.efi"

3.4 Подпись созданного Unified Kernel Image

На данном этапе полученный EFI-образ подписывается ранее созданным Image Signing сертификатом (ISK):

sudo sbsign --key ISK.key --cert ISK.pem /boot/efi/EFI/Linux/linux.efi --output /boot/efi/EFI/Linux/linux.signed.efi

В случае если был создан резервный загрузочный образ, то подписывается и он тоже командой:

sudo sbsign --key ISK.key --cert ISK.pem /boot/efi/EFI/Linux/linux-fallback.efi --output /boot/efi/EFI/Linux/linux-fallback.signed.efi

3.5 Автоматизация генерации Unified Kernel Image

Для дистрибутивов, производных от Debian, автоматическое создание Unified Kernel Image реализуется посредством post-update.d hook'а пакета initramfs-tools.

Для этого создается папка /etc/initramfs/post-update.d командой:

sudo mkdir -p /etc/initramfs/post-update.d

Затем создается скрипт, который будет выполнять генерацию EFI-образа командой:

sudo bash -c "cat > /etc/initramfs/post-update.d/create_unified_kernel" << EOL
#!/bin/sh
mkdir -p /boot/efi/EFI/Linux

efistub_path=\$(case \$(uname -m) in "x86_64") echo "/usr/lib/systemd/boot/efi/linuxx64.efi.stub";; \\
"i686") echo "/usr/lib/systemd/boot/efi/linuxia32.efi.stub";; \\
"aarch64") echo "/usr/lib/systemd/boot/efi/linuxaa64.efi.stub";; \\
*) echo "unsupported arch" \$(uname -m);; esac;)
vmlinuz_path=\$(realpath /vmlinuz)
initrd_path=\$(realpath /initrd.img)
osrelease_path=\$(realpath /etc/os-release)
stub_line=\$(objdump -h \$efistub_path | tail -2 | head -1)
stub_size=0x\$(echo "\$stub_line" | awk '{print \$3}')
stub_offs=0x\$(echo "\$stub_line" | awk '{print \$4}')
osrel_offs=\$((stub_size + stub_offs))
cmdline_offs=\$((osrel_offs + \$(stat -c%s "\$osrelease_path")))
linux_offs=\$((cmdline_offs + \$(stat -c%s "/etc/kernel/cmdline")))
initrd_offs=\$((linux_offs + \$(stat -c%s "\$vmlinuz_path")))

objcopy \\
    --add-section .osrel=\$osrelease_path \\
    --change-section-vma .osrel=\$(printf 0x%x \$osrel_offs) \\
    --add-section .cmdline="/etc/kernel/cmdline" \\
    --change-section-vma .cmdline=\$(printf 0x%x \$cmdline_offs) \\
    --add-section .linux=\$vmlinuz_path \\
    --change-section-vma .linux=\$(printf 0x%x \$linux_offs) \\
    --add-section .initrd=\$initrd_path \\
    --change-section-vma .initrd=\$(printf 0x%x \$initrd_offs) \\
    \$efistub_path "/boot/efi/EFI/Linux/linux.efi" 
EOL

Если вы планируете загружать резервный EFI-образ, то скрипт необходимо расширить, и команда будет выглядеть следующим образом:

sudo bash -c "cat > /etc/initramfs/post-update.d/create_unified_kernel" << EOL
#!/bin/sh
mkdir -p /boot/efi/EFI/Linux

efistub_path=\$(case \$(uname -m) in "x86_64") echo "/usr/lib/systemd/boot/efi/linuxx64.efi.stub";; \\
"i686") echo "/usr/lib/systemd/boot/efi/linuxia32.efi.stub";; \\
"aarch64") echo "/usr/lib/systemd/boot/efi/linuxaa64.efi.stub";; \\
*) echo "unsupported arch" \$(uname -m);; esac;)
vmlinuz_path=\$(realpath /vmlinuz)
initrd_path=\$(realpath /initrd.img)
osrelease_path=\$(realpath /etc/os-release)
stub_line=\$(objdump -h \$efistub_path | tail -2 | head -1)
stub_size=0x\$(echo "\$stub_line" | awk '{print \$3}')
stub_offs=0x\$(echo "\$stub_line" | awk '{print \$4}')
osrel_offs=\$((stub_size + stub_offs))
cmdline_offs=\$((osrel_offs + \$(stat -c%s "\$osrelease_path")))
linux_offs=\$((cmdline_offs + \$(stat -c%s "/etc/kernel/cmdline")))
initrd_offs=\$((linux_offs + \$(stat -c%s "\$vmlinuz_path")))

objcopy \\
    --add-section .osrel=\$osrelease_path \\
    --change-section-vma .osrel=\$(printf 0x%x \$osrel_offs) \\
    --add-section .cmdline="/etc/kernel/cmdline" \\
    --change-section-vma .cmdline=\$(printf 0x%x \$cmdline_offs) \\
    --add-section .linux=\$vmlinuz_path \\
    --change-section-vma .linux=\$(printf 0x%x \$linux_offs) \\
    --add-section .initrd=\$initrd_path \\
    --change-section-vma .initrd=\$(printf 0x%x \$initrd_offs) \\
    \$efistub_path "/boot/efi/EFI/Linux/linux.efi"

vmlinuz_path=\$(realpath /vmlinuz.old)
initrd_path=\$(realpath /initrd.img.old)
initrd_offs=\$((linux_offs + \$(stat -c%s "\$vmlinuz_path")))

objcopy \\
    --add-section .osrel=\$osrelease_path \\
    --change-section-vma .osrel=\$(printf 0x%x \$osrel_offs) \\
    --add-section .cmdline="/etc/kernel/cmdline" \\
    --change-section-vma .cmdline=\$(printf 0x%x \$cmdline_offs) \\
    --add-section .linux=\$vmlinuz_path \\
    --change-section-vma .linux=\$(printf 0x%x \$linux_offs) \\
    --add-section .initrd=\$initrd_path \\
    --change-section-vma .initrd=\$(printf 0x%x \$initrd_offs) \\
    \$efistub_path "/boot/efi/EFI/Linux/linux-fallback.efi"
EOL

После этого необходимо дать скрипту права на исполнение:

sudo chmod a+x /etc/initramfs/post-update.d/create_unified_kernel

Для проверки корректной работы скрипта можно инициировать обновление initramfs командой:

sudo update-initramfs -u -k all

В результате должны создаться EFI-образы по путям /boot/efi/EFI/Linux/linux.efi, /boot/efi/EFI/Linux/linux-fallback.efi.

В дистрибутивах ОС, производных от Arch Linux, существует множество различных способов автоматизации создания Unified Kernel Image, описанных на официальной Wiki.

В данном руководстве рассматривается способ с использованием утилиты mkinitcpio. Подробно про mkinitcpio можно ознакомиться по ссылкам: mkinitcpio[en] | mkinitcpio[ru]

Положительной особенностью является то, что mkinitcpio умеет автоматически склеивать файлы обновления микрокода с initrfamfs с помощью конфигурационного префикса *_microcode (прим. ALL_microcode, _microcode (прим. fallback_microcode)).

Убедитесь, что папка по пути /boot/efi/EFI/Linux присутствует. Если её нет, то создайте её командой:

sudo mkdir -p /boot/efi/EFI/Linux

Файлы-пресеты для Linux-ядер расположены по пути /etc/mkinitcpio.d/. Пример файла-пресета для пакета linux (linux.preset):

# mkinitcpio preset file for the 'linux' package

ALL_config="/etc/mkinitcpio.conf"
ALL_kver="/boot/vmlinuz-linux"
ALL_microcode="/boot/*-ucode.img"

PRESETS=('default')

default_image="/boot/initramfs-linux.img"
default_efi_image="/boot/efi/EFI/Linux/linux.efi"
default_options="--splash /usr/share/systemd/bootctl/splash-arch.bmp"

Файл конфигурации mkinitcpio, который дополнительно генерирует резервный EFI-образ (fallback) linux-lts ядра:

# mkinitcpio preset file for the 'linux' package

ALL_config="/etc/mkinitcpio.conf"
ALL_microcode="/boot/*-ucode.img"

PRESETS=('default' 'fallback')

default_kver="/boot/vmlinuz-linux"
default_image="/boot/initramfs-linux.img"
default_efi_image="/boot/efi/EFI/Linux/linux.efi"
default_options="--splash /usr/share/systemd/bootctl/splash-arch.bmp"

fallback_kver="/boot/vmlinuz-linux-lts"
fallback_efi_image="/boot/efi/EFI/Linux/linux-fallback.efi"
fallback_image="/boot/initramfs-linux-lts-fallback.img"
fallback_options="-S autodetect --splash /usr/share/systemd/bootctl/splash-arch.bmp"

После того как вы создали файл-пресет, его можно проверить, запустив mkinitcpio командой:

sudo mkinitcpio -p linux

В результате должны создаться исполняемые файлы EFI по пути /boot/efi/EFI/Linux.

Заметьте, что при автоматической генерации EFI-образов они не подписываются! Мы считаем, что приватный ключ сертификата подписи ISK.key защищен паролем, что не позволяет осуществить подпись исполняемых файлов в автоматическом режиме без ввода пароля. Поэтому после каждой генерации Unified Kernel Image, его необходимо подписать в ручном режиме, как описано в прошлом пункте.

Теперь нужно создать загрузочную запись Unified Kernel Image.

3.6 Добавление Unified Kernel Image в загрузку

Для этого создается загрузочная запись в EFI Boot Manager командой:

efibootmgr --disk /dev/sda --part 1 --create --label "Linux Signed Boot" --loader /EFI/Linux/linux.signed.efi

Здесь /dev/sda - это диск с системным EFI разделом, а --part 1 указывает порядковый номер этого раздела.

Если ранее Вы создавали резервный EFI-образ, то сделайте дополнительно загрузочную запись и для него командой:

efibootmgr --disk /dev/sda --part 1 --create --label "Linux Fallback Signed Boot" --loader /EFI/Linux/linux-fallback.signed.efi

В случае, если вы указываете конкретную версию ядра Linux в названии образа Unified Kernel Image, то следует это также учесть. Пример команды для текущего ядра:

efibootmgr --disk /dev/sda --part 1 --create --label "Linux $(uname -r) Signed Boot" --loader /EFI/Linux/linux-$(uname -r).efi

Теперь нужно убедиться, что загрузочная запись присутствует в порядке загрузки, и следует указать её в качестве первой - для этого используется аргумент --bootorder:

sudo efibootmgr --bootorder XXXX,YYYY,ZZZZ

здесь XXXX,YYYY,ZZZZ - порядковые номера загрузочных записей.

LVCSecureBoot 15

Если все в порядке, то далее надо выполнить перезагрузку и убедиться в том, что созданный и подписанный образ успешно запускается, после чего предлагается выполнить подпись ядра, его модулей и включить LSM Kernel Lockdown.

4. Подпись ядра, модулей ядра, функция Kernel Lockdown

Данный пункт посвящен настройке элементов замкнутой программный среды (ЗПС) для их сопряжения с механизмом безопасной загрузки. Как правило в состав ЗПС входит:

  • Контроль загрузки модулей ядра - проверка встроенной в модуль подписи при загрузке его в ядро ОС.
  • Контроль исполняемых файлов (библиотек, скриптов и модулей ядра) - проверка подписи, наложенной в расширенных атрибутах файла (Linux IMA/EVM - Integrity Measurement Architecture and Extended Verification Module).

В качестве подготовительных действий следует убедиться в наличии включенных опций ядра, отвечающих за поддержку подписи модулей ядра и ее проверку, а именно:

  • CONFIG_MODULE_SIG
  • CONFIG_MODULE_SIG_HASH
  • CONFIG_INTEGRITY_SIGNATURE

Конфигурационный файл запущенного ядра может быть расположен по-разному в зависимости от дистрибутива, как правило его можно найти по путям:

  • /proc/config.gz
  • /boot/config
  • /boot/config-$(uname -r)
  • /usr/src/linux-headers-$(uname -r)/.config

Для того чтобы проверить, включена та или иная опция, Вы можете воспользоваться командой:

zgrep <Опция> <Путь до файла конфигурации>

В ядре Linux существует несколько хранилищ ключей (keyrings), с ними подробно можно ознакомиться в документации ядра Linux. Для использования ключей Secure Boot в Linux необходимо наличие связки .platform и загрузки публичных ключей из базы разрешенных ключей (db). За формирование этого хранилища ключей отвечают две опции ядра:

  • CONFIG_LOAD_UEFI_KEYS
  • CONFIG_INTEGRITY_PLATFORM_KEYRING

Помимо формирования хранилища ключей .platform, необходимо также наличие патча ядра, который добавляет проверку подписи модулей с использованием публичных сертификатов из него. К сожалению, данная модификация функции mod_verify_sig не была принята в upstream ядра Linux, поэтому большинство дистрибутивов добавляют его в ядро системы самостоятельно (прим. Ubuntu 22.04, Ubuntu 20.04, Fedora 28 и т.д). Помимо этого патча, повлиять на загрузку операционной системы может также патч принудительно включающий режим Kernel Lockdown (см. п.п. 4.3) в UEFI Secure Boot. В случае, если модули ядра не подписаны или отсутствует возможность организации доверенного хранилища для их проверки, режим Kernel Lockdown не позволит загрузить операционную систему, ввиду того, что он включает принудительную проверку подписи ядерных модулей.

В таблице ниже представлен список протестированных дистрибутивов с пометкой соответветствующих особенностей, влияющих на функцию проверки модулей ядра, Kernel Lockdown и работу UEFI Secure Boot в целом:

Особенность/ДистрибутивUbuntu 22.04 "Jammy Jellyfish"; Ubuntu 20.04 "Focal Fossa"Debian 11 "Bullseye", Debian 10 "Buster" (Linux 5.10+)Fedora 28 - 38RHEL 8, RHEL 9Alt Linux WorkStation 10Arch Linux и дистрибутивы производные от него (BlackArch, Manjaro, Garuda, и т.д)RED OS МУРОМ 7.3.1ROSA Fresh Desktop 12.2Astra Linux 1.7.2
Патч mod_verify_sig для проверки подписи модулей ядра открытыми ключами из .platform++++-----
Патч принудительно включающий Kernel Lockdown при активном UEFI Secure Boot (LOCK_DOWN_IN_SECURE_BOOT или LOCK_DOWN_IN_EFI_SECURE_BOOT)++++----+
Модули ядра подписаны на этапе сборки встроенным в ядро CA сертификатом (прим. Build time autogenerated kernel key)+++++++-*
Наличие модуля Kernel Lockdown (SECURITY_LOCKDOWN_LSM)+++++++-+
Установена политика Kernel Lockdown отличная от NONE (LOCK_DOWN_KERNEL_FORCE_INTEGRITY или LOCK_DOWN_KERNEL_FORCE_CONFIDENTIALITY)---------

здесь

  • ____ - Kernel Lockdown и проверка модулей ядра работает с самоподписанными модулями; используются предзагрузочные открытые ключи из базы разрешенных ключей UEFI Secure Boot.
  • ____ - Kernel Lockdown и штатная функция ядра по проверке подписи модулей не работает ввиду того, что используется собственная реализация режима замкнутой программной среды, основанная на не выгружаемом модуле ядра digsig_verif, который использует криптографические алгоритмы ГОСТ. Соответственно модули ядра, как и исполняемые ELF-файлы подписаны ГОСТовым CA сертификатом. Более подробно с настройкой режима замкнутой программной среды в Astra Linux можно ознакомиться в официальной странице Wiki дистрибутива. На текущий момент версия дистрибутива 1.7.2 не совместима с включенным режимом UEFI Secure Boot, ввиду наличия патча принудительно включающего Kernel Lockdown. Дело в том, что Kernel Lockdown форсирует проверку подписи модулей используя механизм ядра, что не совместимо с используемым модулем digsig_verif.
  • ____ - модули ядра подписаны CA сертификатом дистрибутива, вшитым в ядро на этапе компиляции; Kernel Lockdown и проверка модулей работает только с комплектными модулями ядра, проверка самоподписанных пользователем модулей не поддерживается.
  • ____ - комплектные модули ядра не подписаны, проверка модулей ядра не функционирует, может отсутствовать Kernel Lockdown; возможность проверки самоподписанных модулей отсутствует; включение UEFI SecureBoot ломает процесс загрузки.

Таким образом, для того, чтобы использовать проверку самоподписанных модулей ядра публичным сертификатом из хранилища ключей .platform, необходимо наличие патча mod_verify_sig. Для дистрибутивов отмеченных оранжевым цветом в таблице выше, необходимо пропустить пункты руководства, описывающие процедуру подписи модулей ядра, а для отмеченных красным цветом включение проверки модулей невозможно в принципе.

В данном руководстве в качестве примера подпись модулей будет осуществляться ранее созданным ключом Image Signing Key, который защищен паролем. Тем не менее, вы можете создать свою новую ключевую пару без парольной фразы, например, kernel.pem с параметром утилиты openssl -nodes, затем добавить его в базу разрешенных ключей db и подписывать им модули ядра в автоматическом режиме. Но в этом случае вы должны убедиться в том, что он надежно хранится. Ввиду того, что с помощью такого ключа можно подписывать не только модули ядра, но и исполняемые файлы, такие как загрузчики и приложения EFI.

4.1 Подпись ядра Linux

По умолчанию ядро подписывается CA-сертификатом дистрибутива ОС. Убедиться в этом можно, проверив, какие ключи присутствуют в связке ключей .builtin_trusted_keys, с помощью команды:

sudo keyctl list %:.builtin_trusted_keys

В операционной системе Debian 11 вы увидите следующие ключи:

LVCSecureBoot 16

Проверить, какими ключами подписано ядро, можно с помощью sbverify:

sudo sbverify --list /boot/vmlinuz-$(uname -r)

LVCSecureBoot 17

В этом случае ядро подписано ключом Debian, который и присутствует в связке ключей. Поскольку ядро подписано ключом Debian, а не какой-то третьей стороной вроде Canonical, то нет никакой необходимости также добавлять вашу подпись, Secure Boot заработает без ошибок и в такой конфигурации без нареканий.

В любом случае вы можете добавить в него и свою подпись командой:

sudo sbsign --key ISK.key --cert ISK.pem /boot/vmlinuz-$(uname -r) --output /boot/vmlinuz-$(uname -r)

В результате, ядро теперь подписано ключом дистрибутива ОС и вашим Image Signing Key:

LVCSecureBoot 18

Подпись ядра на этом завершена, следующим шагом является подпись модулей ядра.

4.2 Подпись модулей ядра Linux

В первую очередь нужно проверить, подписаны ли модули до вас. Например, в Debian они поставляются подписанные ключом Debian Secure Boot CA, а в Astra Linux модули поставляются не подписанными.

Для проверки нужно выбрать любой модуль, допустим из списка загруженных, посмотреть которые можно командой lsmod:

LVCSecureBoot 19

Рассмотрим на примере модуля vfat. Для этого выведем информацию о его подписанте командой:

sudo modinfo -F signer vfat

LVCSecureBoot 20

Как видно на рисунке выше, модуль подписан Debian Secure Boot CA. Модуль может быть также подписан автоматически сгенерированным ключом в процессе сборки ядра (Build time autogenerated kernel key). А вот в случае, если модуль не подписан, команда вернет пустой результат.

Подпись модуля ядра проверяется ключом из .builtin_trusted_keys и .platform.

Стоит отметить, что подписывать модули ядра надо алгоритмом, который используется текущим ядром. Для этого нужно проверить конфиг ядра, например, с помощью команды:

cat /boot/config-5.10.0-17-amd64   | grep CONFIG_MODULE_SIG

Соответственно, если указан CONFIG_MODULE_SIG_SHA256=y, то для подписи необходимо указывать алгоритм sha256 в качестве аргумента утилиты signfile, т.е. соответствие опций с аргументами следующее:

Опция ядраАргумент signfile
CONFIG_MODULE_SIG_SHA256sha256
CONFIG_MODULE_SIG_SHA224sha224
CONFIG_MODULE_SIG_SHA384sha384
CONFIG_MODULE_SIG_SHA512sha512

Предварительно, стоит удалить старые подписи у всех модулей ядра командой:

sudo find /lib/modules/$(uname -r)/ -name .ko -exec strip --strip-debug '{}' \;

После чего следует убедиться, что подпись удалена, на примере любого модуля, например, vfat:

sudo modinfo -F signer vfat
Затем, для того чтобы подписать все модули ядра разом, воспользуйтесь командами:
export SIG_ALGO="sha256"
export KEYDIR="/root/sb_keys"
read -s KBUILD_SIGN_PIN
export KBUILD_SIGN_PIN

sudo --preserve-env=KBUILD_SIGN_PIN find /usr/lib/modules/$(uname -r)/ -name .ko \
-exec /usr/src/linux-headers-$(uname -r)/scripts/sign-file ${SIG_ALGO} ${KEYDIR}/ISK.key ${KEYDIR}/ISK.pem '{}' \;

unset KBUILD_SIGN_PIN

здесь

  • SIG_ALGO задает хэш-функцию алгоритма подписи, которую ранее вы определили в соответствии с таблицей;
  • KEYDIR указывает путь до папки с вашим ключом и сертификатом подписи ISK;
  • KBUILD_SIGN_PIN - в этой переменной указывается ваша парольная фраза от ISK.key.

Повторная проверка подписи модуля должна вернуть вам ваш CN ключа ISK:

sudo modinfo -F signer vfat

LVCSecureBoot 21

После этого надо перегенерировать initramfs:

sudo update-initramfs -u -k $(uname -r)

sudo mkinitcpio -p linux

sudo make-initrd --kernel=$(uname -r)

В результате выполненных действий были подписаны модули текущего ядра и добавлены в initrafms. Теперь нужно задать политику проверки модулей с помощью аргумента ядра module.sig_enforce. Этот аргумент принимает значения:

  • 0, что означает разрешающую политику в отношении модулей ядра; т.е. загрузка неподписанных модулей разрешена, но ядро и соответствующие непрошедшие проверку модули (дополнительно промаркируются буквой "E") будет помечены как испорченные;
  • 1, включает ограничительную политику, которая запрещает загрузку неподписанных модулей.

Независимо от настройки здесь, если модуль имеет блок подписи, который не может быть проанализирован, он будет сразу же отклонен, если опция module.sig_enforce инициализирована.

В данном руководстве предлагается использовать режим с ограничительной политикой, что соответветствует максимальному уровню защиты, т.е. необходимо добавить module.sig_enforce=1 в аргументы ядра, которые ранее были созданы по пути /etc/kernel/cmdline, и пересоздать Unified Kernel Image.

sudo bash -c "echo -n ' module.sig_enforce=1' >> /etc/kernel/cmdline"

Не забудьте создать новый Unified Kernel Image и подписать вашим сертификатом ISK!

Внимание! Если вы подписали модули ядра вашим сертификатом Secure Boot, то не выполняйте на данном этапе перезагрузку, потому что ваш Unified Kernel Image не запустится, т.к. созданные вами ключи еще не установлены, и UEFI

Secure Boot не переведен в User mode, а значит при загрузке связка ключей .platform будет пуста.

Вообще говоря, module.sig_enforce включается по умолчанию в режиме Kernel Lockdown; про него будет рассказано далее.

4.3 Kernel Lockdown

Kernel Lockdown предназначен для лучшего отделения кода ядра от пользовательских процессов. Она позволяет защитить ядро от суперпользователя, лишая его права изменять код ядра, мешая ему, например, установить руткит. Таким образом, если этот пользователь будет взломан или иным образом скомпрометирован, активация данного режима существенно затруднит для злоумышленника вмешательство в другие части системы.

Kernel Lockdown может быть сконфигурирован на этапе сборки ядра, для конфигурации доступны следующие конфигурационные опции:

  • CONFIG_SECURITY_LOCKDOWN_LSM - включает поддержку модуля Lockdown;
  • CONFIG_SECURITY_LOCKDOWN_LSM_EARLY - запускает его раньше всех модулей безопасности;
  • CONFIG_LOCK_DOWN_KERNEL_FORCE_NONE - lockdown выключен по умолчанию, но может быть включен через аргумент ядра lockdown=<политика>;
  • CONFIG_LOCK_DOWN_KERNEL_FORCE_INTEGRITY - lockdown включен по умолчанию с политикой integrity;
  • CONFIG_LOCK_DOWN_KERNEL_FORCE_CONFIDENTIALITY - lockdown включен по умолчанию с политикой confidentiality;
  • CONFIG_LOCK_DOWN_IN_SECURE_BOOT - включает lockdown автоматически при включенном UEFI Secure Boot.

Почти во всех дистрибутивах CONFIG_LOCK_DOWN_IN_SECURE_BOOT включен, а это значит, что lockdown будет включен автоматически после включения SecureBoot. Обязательно обратите на этот момент внимание! Ведь Lockdown форсирует проверку модулей ядра, и, если они не подписаны, как, например, в дистрибутиве Astra Linux, то оперционная система не загрузится после включения Secure Boot!

Тем не менее, рекомендуется однозначно указать режим Lockdown через аргумент ядра lockdown=. Доступно два режима:

  • integrity: считается наиболее подходящим для большинства случаев;
  • confidentiality: лучше всего подходит для систем, хранящих чувствительные данные, защищённые даже от рут-пользователя. Режим конфиденциальности полностью запрещает администраторам доступ к памяти ядра.

Соответственно, для integrity:

sudo bash -c "echo -n ' lockdown=integrity' >> /etc/kernel/cmdline"

А для включения политики confidentiality:

sudo bash -c "echo -n ' lockdown=confidentiality' >> /etc/kernel/cmdline"

После чего нужно пересоздать Unified Kernel Image. Больше информации про Lockdown доступно в записке его автора Мэттью Гаррета.

Теперь необходимо перейти к конфигурации Secure Boot в Setup Utility и добавлению созданных ранее ключей.

5. Конфигурация UEFI Secure Boot и установка ключей

Конфигурация в Setup Utility сводится к тому, что необходимо перевести Secure Boot в режим настройки (Setup Mode), а хранилище ключей - в Custom, для того, чтобы иметь возможность установить свои сертификаты вместо тех, что поставляются производителем материнской платы. Переход в Setup Mode осуществляется удалением ключа PK, но не все производители встроенного программного обеспечения позволяют это сделать прозрачно.

Требуемые опции для включения Secure Boot в режиме настройки варьируются от прошивки к прошивке. Рекомендуется ознакомиться с документацией производителя материнской платы о возможностях по конфигурации UEFI. Достаточно подробно об этом рассказал в своей статье Укрощаем UEFI SecureBoot Николай Шлей (@coderush).

После того как вы успешно перевели платформу в Setup Mode и включили SecureBoot, настоятельно рекомендуется поставить пароль на прошивку (Supervisor Password) для предотвращения изменений настроек прошивки, в частности, отключения SecureBoot и/или подмены его ключей.

Конфигурация Secure Boot в виртуальных средах требует дополнительных действий, что будет описано в следующем пункте.

5.1 Настройка UEFI Secure Boot в средах виртуализации

5.1.1 VMWare Fusion / VMWare Workstation

Для конфигурации Secure Boot виртуальной машины необходимо добавить следующие строки в файл vmx:

uefi.secureBoot.enabled = "TRUE"
uefi.allowAuthBypass = "TRUE"
bios.forceSetupOnce = "TRUE"

При последующей загрузке виртуальная машина загрузится в окно прошивки из которого нужно зайти в Setup Utility и перевести платформу в Setup Mode, как показано на схеме ниже.

LVCSecureBoot 22

После этого обязательно удалите ранее добавленную строку конфигурации uefi.allowAuthBypass = "TRUE" из файла vmx, предварительно завершив работу виртуальной машины. Теперь можно приступить к установке ключей с помощью KeyTool.

5.1.2 QEMU + OVMF

Для использования UEFI Secure Boot в средстве виртуализации QEMU, требуется проект Open Virtual Machine Firmware (OVMF), который представляет из себя сборку EDK II для виртуальной машины архитектуры x86. Прошивка OVMF реализует полную поддержку стандарта UEFI, включая механизм Secure Boot, позволяя использовать в виртуальной машине UEFI вместо традиционного BIOS (SeaBIOS).

Всё, что требуется в этой связке, - это скомпилировать образ OVMF с параметром -D SECURE_BOOT_ENABLE, который включает поддержку UEFI Secure Boot, как показано ниже:

git clone https://github.com/acidanthera/audk.git
cd audk
git submodule update --init --recursive
. edksetup.sh
make -C BaseTools
build -a X64 -t GCC5 -b RELEASE -p OvmfPkg/OvmfPkgX64.dsc -D SECURE_BOOT_ENABLE

Артефакты сборки находятся в директории Build. Файл прошивки OVMF расположен по пути Build/OvmfX64/RELEASE_GCC5/FV/OVMF.fd.

Пример запуска виртуальной машины с OVMF:

qemu-system-x86_64 -m 2048    -drive if=pflash,format=raw,unit=0,file=Build/OvmfX64/RELEASE_GCC5/FV/OVMF.fd

По умолчанию OVMF находится в режиме Setup Mode, дополнительных настроек в Setup Utility не требуется, поэтому можно переходить сразу установке ключей с помощью KeyTool.

5.1.3 VirtualBox

Механизм UEFI SecureBoot и поддержка TPM доступна только в версии VirtualBox 7.0 и выше. Поэтому, если у вас виртуальная машина с включенным EFI, то можете сразу переходить к установке сертификатов с помощью утилиты KeyTool.

5.2 Установка сертификатов с помощью KeyTool

Выполняется загрузка с ранее подготовленного флеш-накопителя, содержащего утилиту KeyTool и ключи Secure Boot. Из главного меню KeyTool выбирается Edit Keys, затем открывается меню со списком ключей (PK, KEK, db, dbx, dbt, MOK), в котором по очереди с каждым ключом выполняется операция "Replace's Key(s)", как показано на схеме ниже.

LVCSecureBoot 23

В результате система должна перейти в User Mode, и при следующей перезагрузке система будет проверять подпись исполняемых файлов загруженными ключами.

Проверить статус Secure Boot в Linux из операционной системы можно командой:

sudo bootctl status 2>&1 | grep -E 'Secure|Setup'

LVCSecureBoot 24

Заключение

В результате выполненных действий, вы включили безопасную загрузку UEFI с помощью пользовательских ключей, создали Unified Kernel Image и включили функцию Kernel Lock Down. Не забудьте установить пароль на прошивку. Любой, кто получит доступ к Setup Utility, может легко сбросить ваши пользовательские ключи и получить контроль над вашим компьютером.

Обратите внимание, что предложенное решение позволяет устранить только часть недостатков безопасной загрузки, поскольку многие из них возникают на уровне архитектуры прошивки и настройками дистрибутива ОС их не устранить. Подробно про архитектурные проблемы рассказывается в нашем докладе Безопасная загрузка ядра Linux в UEFI окружении: проблемы и перспективы на конференции OS DAY 2022.