From 0ca92bea0c06c3d1480c304cd8fc5ae7f44b9138 Mon Sep 17 00:00:00 2001 From: Joey Vagedes Date: Wed, 16 Aug 2023 16:14:16 -0700 Subject: [PATCH 01/12] Initial commit --- .github/dependabot.yml | 24 + .github/workflows/BuildBinaries.yml | 42 ++ .github/workflows/release.yml | 16 + .github/workflows/run-ci.yml | 14 + .gitignore | 5 +- Readme.md | 109 +++- keystore/Db/MicCorUEFCA2011_2011-06-27.crt | Bin 0 -> 1556 bytes keystore/Db/MicWinProPCA2011_2011-10-19.crt | Bin 0 -> 1499 bytes keystore/Db/microsoft uefi ca 2023.crt | 1 + keystore/Db/windows uefi ca 2023.crt | 1 + keystore/Dbx/dbx_info_uefi_org_7_18_23.csv | 521 ++++++++++++++++++ keystore/Kek/MicCorKEKCA2011_2011-06-24.crt | Bin 0 -> 1516 bytes .../microsoft corporation kek 2k ca 2023.crt | 1 + keystore/keystore.toml | 75 +++ ruff.toml | 13 + scripts/prepare.py | 55 ++ scripts/secure_boot_default_keys.py | 325 +++++++++++ 17 files changed, 1200 insertions(+), 2 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/BuildBinaries.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/run-ci.yml create mode 100644 keystore/Db/MicCorUEFCA2011_2011-06-27.crt create mode 100644 keystore/Db/MicWinProPCA2011_2011-10-19.crt create mode 100644 keystore/Db/microsoft uefi ca 2023.crt create mode 100644 keystore/Db/windows uefi ca 2023.crt create mode 100644 keystore/Dbx/dbx_info_uefi_org_7_18_23.csv create mode 100644 keystore/Kek/MicCorKEKCA2011_2011-06-24.crt create mode 100644 keystore/Kek/microsoft corporation kek 2k ca 2023.crt create mode 100644 keystore/keystore.toml create mode 100644 ruff.toml create mode 100644 scripts/prepare.py create mode 100644 scripts/secure_boot_default_keys.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a673683 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,24 @@ +## @file +# Dependabot configuration file to enable GitHub services for managing and updating +# dependencies. +# +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: BSD-2-Clause-Patent +# +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +## +version: 2 +updates: + - package-ecosystem: "pip" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + day: "monday" + time: "01:00" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "01:00" \ No newline at end of file diff --git a/.github/workflows/BuildBinaries.yml b/.github/workflows/BuildBinaries.yml new file mode 100644 index 0000000..72265b6 --- /dev/null +++ b/.github/workflows/BuildBinaries.yml @@ -0,0 +1,42 @@ +name: Build SecureBoot Binaries + +on: + workflow_call: + inputs: + python-version: + description: 'Python Version' + default: '3.11' + +jobs: + build: + name: Build + + runs-on: ubuntu-latest + + steps: + - name: Checkout Self + uses: actions/checkout@v3 + + - name: Set up Python ${{ inputs.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ inputs.python-version }} + cache: 'pip' + + - name: Install Pip Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run Ruff Checks + run: ruff check scripts --format=github + + - name: Build Intel / ARM Binaries + run: python scripts/secure_boot_default_keys.py --keystore keystore/keystore.toml -o artifacts + + - name: Upload Binaries as Artifact + uses: actions/upload-artifact@v3 + with: + name: binaries + path: artifacts/ + \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1eb253b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,16 @@ +# This workflow builds and zips the secureboot binaries and uploads them to the releases page. +# +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: BSD-2-Clause-Patent + +name: SecureBoot Release + +on: + release: + types: [published] + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: diff --git a/.github/workflows/run-ci.yml b/.github/workflows/run-ci.yml new file mode 100644 index 0000000..9720f61 --- /dev/null +++ b/.github/workflows/run-ci.yml @@ -0,0 +1,14 @@ +name: PR Check + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + name: Build Binaries + uses: ./.github/workflows/BuildBinaries.yml + with: + python-version: '3.11' \ No newline at end of file diff --git a/.gitignore b/.gitignore index d15fa6d..c0206a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ -Artifacts/* +**/*.zip +**/*.tar.gz +**/*.bin +**/README.md Pipfile .ruff_cache/* diff --git a/Readme.md b/Readme.md index f49711d..32540d8 100644 --- a/Readme.md +++ b/Readme.md @@ -1,3 +1,110 @@ # Secure Boot Objects -This repository is used to hold the secure boot objects recommended by Microsoft such as the KEK, DB, and DBX. +This repository is used to hold the secure boot objects recommended by +Microsoft to use for your KEK, DB, and DBX. This repository utilizes a script +(`scripts/secure_boot_default_keys.py`) to generate the binary blobs based off +of the `keystore.toml` configuration file. + +On a release github release, the script is run and the generated binaries are +bundled, zipped, and attached to the release so that they can be consumed by +platforms. + +## Transparency + +By Keeping the contents of the KeK, Db, and Dbx in human readable form in this +repository, it enables developers to easily review the contents and make +changes as needed. This also enables an easy way for the KeK, Db, and (mainly) +the Dbx to be updated transparently and then consumed by any platform! + +## Platform Consumption + +The secure boot binary objects are formatted to the expected EDKII data +structures to enable simple integration into any platform. Please refer to +[SecureBootKeyStoreLibOem](https://github.com/microsoft/mu_oem_sample/tree/release/202302/OemPkg/Library/SecureBootKeyStoreLibOem) +to show one example on how to easily integrate these binaries into your +platform. The EDKII build system even supports easily adding to the default +values suggested by Microsoft! + +## secure_boot_default_keys.py + +This script ingests a configuration file and generates binary blobs for each +table specified in the configuration file. + +``` cmd +usage: secure_boot_default_keys.py [-h] --keystore KEYSTORE [-o OUTPUT] + +Build the default keys for secure boot. + +options: + -h, --help show this help message and exit + --keystore KEYSTORE A json file containing the keys mapped to certificates and + hashes. + -o OUTPUT, --output OUTPUT + The output directory for the default keys. +``` + +## Configuration File + +A configuration file must be provided to the script to generate the binary +information. The script generates a binary blob for each table entry in the +toml file (a table is each `[]` in the toml file) and for each supported +architecture (Currently Intel, ARM). We also generate a readme for each +architecture to provide information about the files inside each binary blob + +That is to say, if you define a toml file similar to this: + +```toml +[MyCustomKek] + +[MyCustomDb] + +[MyCustomDbx] +``` + +Binary blobs will be created with the following folder structure: + +``` cmd +Artifacts +├── Aarch64 +│ ├── MyCustomKek.bin +│ ├── MyCustomDb.bin +│ ├── MyCustomDbx.bin +│ └── README.md +├── Arm +│ ├── MyCustomKek.bin +│ ├── MyCustomDb.bin +│ ├── MyCustomDbx.bin +│ └── README.md +├── Ia32 +│ ├── MyCustomKek.bin +│ ├── MyCustomDb.bin +│ ├── MyCustomDbx.bin +│ └── README.md +└── X64 + ├── MyCustomKek.bin + ├── MyCustomDb.bin + ├── MyCustomDbx.bin + └── README.md +``` + +For each table in the toml file, the script supports the following entries: + +1. `help (Optional)`: A short blob of information to be added to the + readme for that table entry. +2. `arch (Optional)`: The architecture (Intel, ARM) the blob should be + generated for. Defaults to all. +3. `file (list)`: A list of files to include in the binary blob (.crt, + .csv). This has additional config described below +4. `signature_owner (Optional)`: The GUID of the signature owner. + +For each file in the toml file, the script supports the following entries: + +1. `path (str)`: The local path to the file to include in the binary +2. `url (Optional)`: The url to where the file was downloaded from. + Included in the readme if provided +3. `sha1 (Optional)`: The sha1 hash of the file. Included in the readme + if provided. + +## TODO + +1. make `path` optional and add support for downloading the file from the url. diff --git a/keystore/Db/MicCorUEFCA2011_2011-06-27.crt b/keystore/Db/MicCorUEFCA2011_2011-06-27.crt new file mode 100644 index 0000000000000000000000000000000000000000..9aa6ac6c79b21cf1c23b01b5a747aea6ca15da37 GIT binary patch literal 1556 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 int: + """Entry point for the script.""" + parser = argparse.ArgumentParser( + description="Organizes and zips the files for a release.") + parser.add_argument("input", type=pathlib.Path, + help="The directory containing the files to be Prepared.") + parser.add_argument("--version", required=True, + help="The version number of the release.") + parser.add_argument("-o","--output", default="Archives", type = pathlib.Path, + help="The output directory for prepared files.") + args = parser.parse_args() + + out_path = args.output + in_path = args.input + # Make directory if it doesn't exist. Delete any files in it if it does. + out_path.mkdir(parents=True, exist_ok=True) + for file_path in out_path.rglob("*"): + if file_path.is_file(): + file_path.unlink() + + for key, value in LAYOUT.items(): + tmp_dir = tempfile.TemporaryDirectory() + pathlib.Path(tmp_dir.name, "VERSION").write_text(args.version) + for arch in value: + if not (in_path / arch).exists(): + raise RuntimeError(f"Missing {arch} directory in {in_path}") + shutil.copytree(in_path / arch, pathlib.Path(tmp_dir.name, arch)) + + shutil.make_archive(out_path / key, "zip", tmp_dir.name) + shutil.make_archive(out_path / key, "gztar", tmp_dir.name) + + +if __name__ == "__main__": + + logging.basicConfig(level=logging.INFO, + format="%(levelname)s: %(message)s") + sys.exit(main()) diff --git a/scripts/secure_boot_default_keys.py b/scripts/secure_boot_default_keys.py new file mode 100644 index 0000000..c53d844 --- /dev/null +++ b/scripts/secure_boot_default_keys.py @@ -0,0 +1,325 @@ +# @file +# +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: BSD-2-Clause-Patent +## +"""A command line script used to build the authenticated variable structures for Secureboot.""" + +import base64 +import csv +import logging +import uuid +from pathlib import Path +from tempfile import TemporaryFile +from typing import Union + +from edk2toollib.uefi.authenticated_variables_structure_support import ( + EfiSignatureDataEfiCertSha256, + EfiSignatureDataFactory, + EfiSignatureList, +) + +DEFAULT_MS_SIGNATURE_GUID = "77fa9abd-0359-4d32-bd60-28f4e78f784b" +ARCH_MAP = { + "64-bit": "x64", + "32-bit": "ia32", + "32-bit ARM": "arm", + "64-bit ARM": "aarch64" +} + +def _is_pem_encoded(certificate_data: Union[str, bytes]) -> bool: + """This function is used to check if a certificate is pem encoded (base64 encoded). + + Args: + certificate_data (str | bytes): The certificate to check. + + Returns: + bool: True if the certificate is pem encoded, False otherwise. + """ + try: + if isinstance(certificate_data, str): + # If there's any unicode here, an exception will be thrown and the function will return false + sb_bytes = bytes(certificate_data, 'ascii') + elif isinstance(certificate_data, bytes): + sb_bytes = certificate_data + else: + raise ValueError("Argument must be string or bytes") + + return base64.b64encode(base64.b64decode(sb_bytes)) == sb_bytes + except Exception: + return False + +def _convert_pem_to_der(certificate_data: Union[str, bytes]) -> bytes: + """This function is used to convert a pem encoded certificate to a der encoded certificate. + + Args: + certificate_data: The certificate to convert. + + Returns: + bytes: The der encoded certificate. + """ + if isinstance(certificate_data, str): + # If there's any unicode here, an exception will be thrown and the function will return false + certificate_data = bytes(certificate_data, 'ascii') + + return base64.b64decode(certificate_data) + +def _invalid_file(file: str, **kwargs: any) -> None: + """This function is used to handle invalid filetypes. + + Args: + file: The path to the file + + Optional Args: + **kwargs: Additional arguments to be passed to the function (These will be intentionally ignored) + + Raises: + ValueError: If the file is invalid, raise a ValueError. + """ + raise ValueError(f"Invalid filetype for conversion: {file}") + + +def _convert_crt_to_signature_list(file: str, signature_owner: str=DEFAULT_MS_SIGNATURE_GUID, **kwargs: any) -> bytes: + """This function converts a single crt file to a signature list. + + Args: + file: The path to the crt file + signature_owner: The signature owner. Defaults to DEFAULT_MS_SIGNATURE_GUID. + + Optional Args: + **kwargs: Additional arguments to be passed to the function (These will be intentionally ignored) + + Returns: + bytes: The signature list + """ + if signature_owner is not None and not isinstance(signature_owner, uuid.UUID): + signature_owner = uuid.UUID(signature_owner) + + siglist = EfiSignatureList( + typeguid=EfiSignatureDataFactory.EFI_CERT_X509_GUID) + + with open(file, "rb") as crt_file, TemporaryFile() as temp_file: + + certificate = crt_file.read() + if _is_pem_encoded(certificate): + certificate = _convert_pem_to_der(certificate) + + temp_file.write(certificate) + temp_file.seek(0) + + sigdata = EfiSignatureDataFactory.create( + EfiSignatureDataFactory.EFI_CERT_X509_GUID, + temp_file, + signature_owner) + + # X.509 certificates are variable size, so they must be contained in their own signature list + siglist.AddSignatureHeader(None, SigSize=sigdata.get_total_size()) + siglist.AddSignatureData(sigdata) + + return siglist.encode() + + +def _convert_csv_to_signature_list( + file: str, + signature_owner: str=DEFAULT_MS_SIGNATURE_GUID, + target_arch:str=None, + **kwargs: any +) -> bytes: + """This function is used to handle the csv files. + + This function expects to be given a csv file with the following format: + SHA 256 FLAT, PE256 Authenticode, filename, Architecture, Partner, CVEs, Revocation List Date + + This file may be found on uefi.org/revocationlistfile + + Args: + file: The path to the crt file + signature_owner: The signature owner. Defaults to DEFAULT_MS_SIGNATURE_GUID. + target_arch: the arch to filter on when parsing the csv + + Optional Args: + **kwargs: Additional arguments to be passed to the function (These will be intentionally ignored) + + Returns: + bytes: The signature list + """ + if signature_owner is not None and not isinstance(signature_owner, uuid.UUID): + signature_owner = uuid.UUID(signature_owner) + + siglist = EfiSignatureList( + typeguid=EfiSignatureDataFactory.EFI_CERT_SHA256_GUID) + + with open(file, "r") as csv_file: + csv_reader = csv.reader(csv_file, delimiter=",") + for i, row in enumerate(csv_reader): + if i == 0: + siglist.AddSignatureHeader( + None, SigSize=EfiSignatureDataEfiCertSha256.STATIC_STRUCT_SIZE) + continue + + authenticode_hash = row[1] + architecture = ARCH_MAP.get(row[3], None) + + if architecture is None: + raise ValueError(f"Invalid architecture: {architecture}") + if target_arch is not None and architecture != target_arch: + logging.debug(f"Skipping {architecture} because it is not in the target architectures.") + continue + + sigdata = EfiSignatureDataEfiCertSha256( + None, None, bytearray.fromhex(authenticode_hash), sigowner = signature_owner) + siglist.AddSignatureData(sigdata) + + return siglist.encode() + + +def build_default_keys(keystore: dict) -> dict: + """This function is used to build the default keys for secure boot. + + Args: + keystore: A [variable, arch] keyed dictionary containing the matching file in hex format + + Returns: + a dictionary containing the hex representation of the file + + """ + logging.info("Building default keys for secure boot.") + + default_keys = {} + + # Add handlers here for different file types. + file_handler = { + ".crt": _convert_crt_to_signature_list, + '.csv': _convert_csv_to_signature_list + } + + # The json file should be a list of signatures including the owner of the signature. + for variable in keystore: + for arch in set(ARCH_MAP.values()): + + # Skip generating this blob if arch is specified and it does not match + if keystore[variable].get("arch", arch) != arch: + logging.debug(f"Skipping {variable} for {arch} due to config file settings.") + continue + + # The signature database is a byte array that will be added to the default keys. + signature_database = bytes() + + signature_owner = keystore[variable].get( + "signature_owner", "77fa9abd-0359-4d32-60bd-28f4e78f784b") + files = keystore[variable]["files"] + # The files should be handled differently depending on the file extension. + for file_dict in files: + # Get the file extension.\ + file_path = Path(file_dict["path"]) + file_ext = file_path.suffix.lower() + + convert_handler = file_handler.get(file_ext, _invalid_file) + + logging.info("Converting %s to signature list.", file_path) + + signature_database += convert_handler( + file=file_path, + signature_owner=signature_owner, + target_arch=arch + ) + + logging.info( + "Appended %s to signature database for variable %s.", file_path, variable) + + default_keys[arch, variable] = signature_database + + logging.debug("Signature Database for %s:", variable) + + return default_keys + +def create_readme(keystore: dict, arch: str) -> str: + """Generates a README.md file for a given architecture. + + Args: + keystore: A dictionary containing the keys mapped to certificates and hashes. + arch: The architecture to filter on when creating the readme + + Returns: + a string representing the readme. + """ + readme = f"""# {arch.capitalize()} Secure Boot Defaults + +This external dependency contains the default values suggested by microsoft the KEK, DB, and DBX UEFI variables. + +1. The KEK (Key Exchange Key) is a list of certificates that verify the signature of other keys attempting to update the DB and DBX. +2. The DB (Signature Database) is a list of certificates that verify the signature of a binary attempting to execute on the system. +3. The DBX (Forbidden Signature Database) is a list of signatures that are forbidden from executing on the system. + +Please review [Microsoft's documentation](https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/windows-secure-boot-key-creation-and-management-guidance?view=windows-11#15-keys-required-for-secure-boot-on-all-pcs) +for more information on key requirements if appending to the defaults provided in this external dependency. +""" # noqa: E501 + for key, value in keystore.items(): + + # Filter out Tables not used for the specific architecture + if keystore[key].get("arch", arch) != arch: + logging.debug(f"Skipping {key} for {arch} due to config file settings.") + continue + readme += f"\n## {key}\n\n" + + if value.get("help", ""): + readme += f"{value.get('help', '')}\n\n" + readme += "Files Included:\n\n" + + for file_dict in value["files"]: + # Filter out Files not used for the specific architecture + if file_dict.get("arch", arch) != arch: + continue + if file_dict.get("url", None) is not None: + readme += f"* <{file_dict['url']}>\n" + return bytes(readme, "utf-8") + +def main() -> int: + """Main entry point into the tool.""" + import argparse + import pathlib + + import tomllib + + parser = argparse.ArgumentParser( + description="Build the default keys for secure boot.") + parser.add_argument("--keystore", help="A json file containing the keys mapped to certificates and hashes.", + default="keystore.toml", required=True) + parser.add_argument("-o", "--output", type=pathlib.Path, default=pathlib.Path.cwd() / "Artifacts", + help="The output directory for the default keys.") + + args = parser.parse_args() + + with open(args.keystore, "rb") as f: + keystore = tomllib.load(f) + + # Build the default key binaries; filters on requested architectures in the configuration file. + default_keys = build_default_keys(keystore) + # Write the keys to the output directory and create a README.md file for each architecture. + for key, value in default_keys.items(): + arch, variable = key + + out_dir = Path(args.output, arch.capitalize()) + + out_dir.mkdir(exist_ok=True, parents=True) + out_dir.touch() + + out_file = Path(out_dir, f"{variable}.bin") + if out_file.exists(): + out_file.unlink() + with open(out_file, "wb") as f: + f.write(value) + + readme_path = Path(out_dir, "README.md") + if readme_path.exists(): + readme_path.unlink() + with open(readme_path, "wb") as f: + f.write(create_readme(keystore, arch)) + return 0 + + +if __name__ == "__main__": + import sys + logging.basicConfig(level=logging.INFO, + format="%(levelname)s: %(message)s") + sys.exit(main()) From dc817f799285cfad5c66330249686be42580295c Mon Sep 17 00:00:00 2001 From: Joey Vagedes Date: Wed, 16 Aug 2023 16:17:04 -0700 Subject: [PATCH 02/12] update --- .github/workflows/run-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-ci.yml b/.github/workflows/run-ci.yml index 9720f61..7208219 100644 --- a/.github/workflows/run-ci.yml +++ b/.github/workflows/run-ci.yml @@ -11,4 +11,4 @@ jobs: name: Build Binaries uses: ./.github/workflows/BuildBinaries.yml with: - python-version: '3.11' \ No newline at end of file + python-version: '3.11' From 9d4aa38a37ce4ceb283e97e18c1db0dfae7c6c68 Mon Sep 17 00:00:00 2001 From: Joey Vagedes Date: Wed, 16 Aug 2023 16:20:32 -0700 Subject: [PATCH 03/12] update --- .github/workflows/run-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-ci.yml b/.github/workflows/run-ci.yml index 7208219..3e02b2c 100644 --- a/.github/workflows/run-ci.yml +++ b/.github/workflows/run-ci.yml @@ -2,9 +2,9 @@ name: PR Check on: push: - branches: [ "master" ] + branches: [ "main" ] pull_request: - branches: [ "master" ] + branches: [ "main" ] jobs: build: From ea9bb2bd58d70ccc1c0b665df3999d164f429019 Mon Sep 17 00:00:00 2001 From: Joey Vagedes Date: Wed, 16 Aug 2023 16:56:27 -0700 Subject: [PATCH 04/12] update (#2) update --- .github/workflows/BuildBinaries.yml | 25 ++++++++++++++++--------- .github/workflows/pr-check.yml | 19 +++++++++++++++++++ .github/workflows/release.yml | 5 ++--- .github/workflows/run-ci.yml | 14 -------------- 4 files changed, 37 insertions(+), 26 deletions(-) create mode 100644 .github/workflows/pr-check.yml delete mode 100644 .github/workflows/run-ci.yml diff --git a/.github/workflows/BuildBinaries.yml b/.github/workflows/BuildBinaries.yml index 72265b6..fa5e297 100644 --- a/.github/workflows/BuildBinaries.yml +++ b/.github/workflows/BuildBinaries.yml @@ -2,10 +2,7 @@ name: Build SecureBoot Binaries on: workflow_call: - inputs: - python-version: - description: 'Python Version' - default: '3.11' + jobs: build: @@ -17,10 +14,10 @@ jobs: - name: Checkout Self uses: actions/checkout@v3 - - name: Set up Python ${{ inputs.python-version }} + - name: Set up Python uses: actions/setup-python@v4 with: - python-version: ${{ inputs.python-version }} + python-version: 3.11 cache: 'pip' - name: Install Pip Dependencies @@ -32,11 +29,21 @@ jobs: run: ruff check scripts --format=github - name: Build Intel / ARM Binaries - run: python scripts/secure_boot_default_keys.py --keystore keystore/keystore.toml -o artifacts + run: python scripts/secure_boot_default_keys.py --keystore keystore/keystore.toml -o Artifacts - name: Upload Binaries as Artifact uses: actions/upload-artifact@v3 with: - name: binaries - path: artifacts/ + name: Binaries + path: Artifacts/ + + - name: Prepare Release Artifacts + run: python scripts/prepare.py Artifacts --output ReleaseArtifacts --version ${{ github.event.release.tag_name }} + if: startsWith(github.ref, 'refs/tags/') + + - name: Upload Release Artifacts + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + files: ReleaseArtifacts/* \ No newline at end of file diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml new file mode 100644 index 0000000..2cd711c --- /dev/null +++ b/.github/workflows/pr-check.yml @@ -0,0 +1,19 @@ +# This workflow verifies the python scripts and that the binary blobs +# can be successfully built. +# +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: BSD-2-Clause-Patent + +name: PR Check + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + name: Build Binaries + uses: ./.github/workflows/BuildBinaries.yml + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1eb253b..7515ea5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,6 +11,5 @@ on: jobs: build: - name: Build - runs-on: ubuntu-latest - steps: + name: Build Binaries + uses: ./.github/workflows/BuildBinaries.yml diff --git a/.github/workflows/run-ci.yml b/.github/workflows/run-ci.yml deleted file mode 100644 index 3e02b2c..0000000 --- a/.github/workflows/run-ci.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: PR Check - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -jobs: - build: - name: Build Binaries - uses: ./.github/workflows/BuildBinaries.yml - with: - python-version: '3.11' From e13c74a0c4718500f56411d770d1790fad50f74b Mon Sep 17 00:00:00 2001 From: Joey Vagedes Date: Thu, 17 Aug 2023 08:38:26 -0700 Subject: [PATCH 05/12] Pr3 (#3) Updates --- .github/workflows/pr-check.yml | 19 ------------- ...BuildBinaries.yml => prepare-binaries.yml} | 28 ++++++++++++++++--- .github/workflows/release.yml | 15 ---------- Readme.md | 4 +++ scripts/prepare.py | 2 +- 5 files changed, 29 insertions(+), 39 deletions(-) delete mode 100644 .github/workflows/pr-check.yml rename .github/workflows/{BuildBinaries.yml => prepare-binaries.yml} (59%) delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml deleted file mode 100644 index 2cd711c..0000000 --- a/.github/workflows/pr-check.yml +++ /dev/null @@ -1,19 +0,0 @@ -# This workflow verifies the python scripts and that the binary blobs -# can be successfully built. -# -# Copyright (c) Microsoft Corporation. -# SPDX-License-Identifier: BSD-2-Clause-Patent - -name: PR Check - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -jobs: - build: - name: Build Binaries - uses: ./.github/workflows/BuildBinaries.yml - diff --git a/.github/workflows/BuildBinaries.yml b/.github/workflows/prepare-binaries.yml similarity index 59% rename from .github/workflows/BuildBinaries.yml rename to .github/workflows/prepare-binaries.yml index fa5e297..843333b 100644 --- a/.github/workflows/BuildBinaries.yml +++ b/.github/workflows/prepare-binaries.yml @@ -1,12 +1,32 @@ -name: Build SecureBoot Binaries - +# This workflow call is responsible for building the secure boot binaries and +# uploading them as a build artifact. This is for PR Checks. +# +# If the workflow call is triggered by a release, (i.e. a tag push), then it +# will additionally archive them (zip, tar.gz) and upload the archives to the +# release as an asset. +# +# NOTE: The GITHUB_TOKEN is used by the action-gh-release@v1 action to upload +# the archives to the release, and thus must have Read and Write +# permissions. +# +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: BSD-2-Clause-Patent +name: Prepare Secure Boot Binaries on: - workflow_call: - + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + release: + types: [published] jobs: build: name: Build + + permissions: + actions: write + contents: read runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 7515ea5..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,15 +0,0 @@ -# This workflow builds and zips the secureboot binaries and uploads them to the releases page. -# -# Copyright (c) Microsoft Corporation. -# SPDX-License-Identifier: BSD-2-Clause-Patent - -name: SecureBoot Release - -on: - release: - types: [published] - -jobs: - build: - name: Build Binaries - uses: ./.github/workflows/BuildBinaries.yml diff --git a/Readme.md b/Readme.md index 32540d8..440acde 100644 --- a/Readme.md +++ b/Readme.md @@ -25,6 +25,10 @@ to show one example on how to easily integrate these binaries into your platform. The EDKII build system even supports easily adding to the default values suggested by Microsoft! +This is just one suggested way of consuming the binary objects. There are of +course plenty of others, such as storing them as a Freeform Ffs file in an +FV. + ## secure_boot_default_keys.py This script ingests a configuration file and generates binary blobs for each diff --git a/scripts/prepare.py b/scripts/prepare.py index 90ae204..fd4f04f 100644 --- a/scripts/prepare.py +++ b/scripts/prepare.py @@ -38,7 +38,7 @@ def main() -> int: for key, value in LAYOUT.items(): tmp_dir = tempfile.TemporaryDirectory() - pathlib.Path(tmp_dir.name, "VERSION").write_text(args.version) + pathlib.Path(tmp_dir.name, "version").write_text(args.version) for arch in value: if not (in_path / arch).exists(): raise RuntimeError(f"Missing {arch} directory in {in_path}") From 4d845e47bd320eb8de0bbb4df85db462117e5158 Mon Sep 17 00:00:00 2001 From: Joey Vagedes Date: Thu, 17 Aug 2023 08:42:15 -0700 Subject: [PATCH 06/12] Update (#4) --- .github/workflows/prepare-binaries.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prepare-binaries.yml b/.github/workflows/prepare-binaries.yml index 843333b..8ded6c7 100644 --- a/.github/workflows/prepare-binaries.yml +++ b/.github/workflows/prepare-binaries.yml @@ -26,7 +26,7 @@ jobs: permissions: actions: write - contents: read + contents: write runs-on: ubuntu-latest From d60d2ae441fcf14742c781a4eda267f3bf54487e Mon Sep 17 00:00:00 2001 From: Joey Vagedes Date: Thu, 17 Aug 2023 08:59:00 -0700 Subject: [PATCH 07/12] Pr5 (#5) Update --- .github/dependabot.yml | 2 +- .github/workflows/prepare-binaries.yml | 13 ++++++------- Readme.md | 14 +++++++------- requirements.txt | 2 +- scripts/prepare.py | 2 +- scripts/secure_boot_default_keys.py | 1 - 6 files changed, 16 insertions(+), 18 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a673683..01f48f8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -21,4 +21,4 @@ updates: schedule: interval: "weekly" day: "monday" - time: "01:00" \ No newline at end of file + time: "01:00" diff --git a/.github/workflows/prepare-binaries.yml b/.github/workflows/prepare-binaries.yml index 8ded6c7..2cccd20 100644 --- a/.github/workflows/prepare-binaries.yml +++ b/.github/workflows/prepare-binaries.yml @@ -23,7 +23,7 @@ on: jobs: build: name: Build - + permissions: actions: write contents: write @@ -33,13 +33,13 @@ jobs: steps: - name: Checkout Self uses: actions/checkout@v3 - + - name: Set up Python uses: actions/setup-python@v4 with: python-version: 3.11 cache: 'pip' - + - name: Install Pip Dependencies run: | python -m pip install --upgrade pip @@ -50,20 +50,19 @@ jobs: - name: Build Intel / ARM Binaries run: python scripts/secure_boot_default_keys.py --keystore keystore/keystore.toml -o Artifacts - + - name: Upload Binaries as Artifact uses: actions/upload-artifact@v3 with: name: Binaries path: Artifacts/ - + - name: Prepare Release Artifacts run: python scripts/prepare.py Artifacts --output ReleaseArtifacts --version ${{ github.event.release.tag_name }} if: startsWith(github.ref, 'refs/tags/') - + - name: Upload Release Artifacts uses: softprops/action-gh-release@v1 if: startsWith(github.ref, 'refs/tags/') with: files: ReleaseArtifacts/* - \ No newline at end of file diff --git a/Readme.md b/Readme.md index 440acde..883e950 100644 --- a/Readme.md +++ b/Readme.md @@ -1,9 +1,9 @@ # Secure Boot Objects This repository is used to hold the secure boot objects recommended by -Microsoft to use for your KEK, DB, and DBX. This repository utilizes a script -(`scripts/secure_boot_default_keys.py`) to generate the binary blobs based off -of the `keystore.toml` configuration file. +Microsoft to use as the default KEK, DB, and DBX variables. This repository +utilizes a script (`scripts/secure_boot_default_keys.py`) to generate the +binary blobs based off of the `keystore.toml` configuration file. On a release github release, the script is run and the generated binaries are bundled, zipped, and attached to the release so that they can be consumed by @@ -11,8 +11,8 @@ platforms. ## Transparency -By Keeping the contents of the KeK, Db, and Dbx in human readable form in this -repository, it enables developers to easily review the contents and make +By Keeping the contents of the KeK, Db, and Dbx in a human readable form in +this repository, it enables developers to easily review the contents and make changes as needed. This also enables an easy way for the KeK, Db, and (mainly) the Dbx to be updated transparently and then consumed by any platform! @@ -21,8 +21,8 @@ the Dbx to be updated transparently and then consumed by any platform! The secure boot binary objects are formatted to the expected EDKII data structures to enable simple integration into any platform. Please refer to [SecureBootKeyStoreLibOem](https://github.com/microsoft/mu_oem_sample/tree/release/202302/OemPkg/Library/SecureBootKeyStoreLibOem) -to show one example on how to easily integrate these binaries into your -platform. The EDKII build system even supports easily adding to the default +to see one example on how to easily integrate these binaries into your +platform. The EDKII build system even supports easily appending to the default values suggested by Microsoft! This is just one suggested way of consuming the binary objects. There are of diff --git a/requirements.txt b/requirements.txt index 9858337..2948176 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ edk2-pytool-library==0.15.3 edk2-pytool-extensions==0.23.9 -ruff==0.0.280 \ No newline at end of file +ruff==0.0.280 diff --git a/scripts/prepare.py b/scripts/prepare.py index fd4f04f..c9040b8 100644 --- a/scripts/prepare.py +++ b/scripts/prepare.py @@ -13,7 +13,7 @@ LAYOUT = { "edk2-arm-secureboot-binaries": ["Aarch64", "Arm"], - "edk2-intel-secureboot-bianries": ["Ia32", "X64"], + "edk2-intel-secureboot-binaries": ["Ia32", "X64"], } def main() -> int: diff --git a/scripts/secure_boot_default_keys.py b/scripts/secure_boot_default_keys.py index c53d844..8478e69 100644 --- a/scripts/secure_boot_default_keys.py +++ b/scripts/secure_boot_default_keys.py @@ -4,7 +4,6 @@ # SPDX-License-Identifier: BSD-2-Clause-Patent ## """A command line script used to build the authenticated variable structures for Secureboot.""" - import base64 import csv import logging From 3ca9593df58ec4aeeb36c388e09cbe00ee523894 Mon Sep 17 00:00:00 2001 From: Joey Vagedes Date: Wed, 16 Aug 2023 16:14:16 -0700 Subject: [PATCH 08/12] Initial port of contents Performs an initial port of the project into the new repository. This port includes two scripts used to (1) generate the secure boot binaries for Intel and ARM architectures and (2) archive the binaries as .zip and .tar.gz files for each of the architectures. Adds auxillary files for performing PR checks and automated attachment of the archive files to the release, if the workflow is being run due to the creation of a tag. Signed-off-by: Joey Vagedes --- .github/dependabot.yml | 24 + .github/workflows/prepare-binaries.yml | 68 +++ .gitignore | 5 +- Readme.md | 113 +++- keystore/Db/MicCorUEFCA2011_2011-06-27.crt | Bin 0 -> 1556 bytes keystore/Db/MicWinProPCA2011_2011-10-19.crt | Bin 0 -> 1499 bytes keystore/Db/microsoft uefi ca 2023.crt | 1 + keystore/Db/windows uefi ca 2023.crt | 1 + keystore/Dbx/dbx_info_uefi_org_7_18_23.csv | 521 ++++++++++++++++++ keystore/Kek/MicCorKEKCA2011_2011-06-24.crt | Bin 0 -> 1516 bytes .../microsoft corporation kek 2k ca 2023.crt | 1 + keystore/keystore.toml | 75 +++ requirements.txt | 2 +- ruff.toml | 13 + scripts/prepare.py | 55 ++ scripts/secure_boot_default_keys.py | 324 +++++++++++ 16 files changed, 1200 insertions(+), 3 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/prepare-binaries.yml create mode 100644 keystore/Db/MicCorUEFCA2011_2011-06-27.crt create mode 100644 keystore/Db/MicWinProPCA2011_2011-10-19.crt create mode 100644 keystore/Db/microsoft uefi ca 2023.crt create mode 100644 keystore/Db/windows uefi ca 2023.crt create mode 100644 keystore/Dbx/dbx_info_uefi_org_7_18_23.csv create mode 100644 keystore/Kek/MicCorKEKCA2011_2011-06-24.crt create mode 100644 keystore/Kek/microsoft corporation kek 2k ca 2023.crt create mode 100644 keystore/keystore.toml create mode 100644 ruff.toml create mode 100644 scripts/prepare.py create mode 100644 scripts/secure_boot_default_keys.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..01f48f8 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,24 @@ +## @file +# Dependabot configuration file to enable GitHub services for managing and updating +# dependencies. +# +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: BSD-2-Clause-Patent +# +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +## +version: 2 +updates: + - package-ecosystem: "pip" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + day: "monday" + time: "01:00" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "01:00" diff --git a/.github/workflows/prepare-binaries.yml b/.github/workflows/prepare-binaries.yml new file mode 100644 index 0000000..2cccd20 --- /dev/null +++ b/.github/workflows/prepare-binaries.yml @@ -0,0 +1,68 @@ +# This workflow call is responsible for building the secure boot binaries and +# uploading them as a build artifact. This is for PR Checks. +# +# If the workflow call is triggered by a release, (i.e. a tag push), then it +# will additionally archive them (zip, tar.gz) and upload the archives to the +# release as an asset. +# +# NOTE: The GITHUB_TOKEN is used by the action-gh-release@v1 action to upload +# the archives to the release, and thus must have Read and Write +# permissions. +# +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: BSD-2-Clause-Patent +name: Prepare Secure Boot Binaries +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + release: + types: [published] + +jobs: + build: + name: Build + + permissions: + actions: write + contents: write + + runs-on: ubuntu-latest + + steps: + - name: Checkout Self + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.11 + cache: 'pip' + + - name: Install Pip Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run Ruff Checks + run: ruff check scripts --format=github + + - name: Build Intel / ARM Binaries + run: python scripts/secure_boot_default_keys.py --keystore keystore/keystore.toml -o Artifacts + + - name: Upload Binaries as Artifact + uses: actions/upload-artifact@v3 + with: + name: Binaries + path: Artifacts/ + + - name: Prepare Release Artifacts + run: python scripts/prepare.py Artifacts --output ReleaseArtifacts --version ${{ github.event.release.tag_name }} + if: startsWith(github.ref, 'refs/tags/') + + - name: Upload Release Artifacts + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + files: ReleaseArtifacts/* diff --git a/.gitignore b/.gitignore index d15fa6d..c0206a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ -Artifacts/* +**/*.zip +**/*.tar.gz +**/*.bin +**/README.md Pipfile .ruff_cache/* diff --git a/Readme.md b/Readme.md index f49711d..883e950 100644 --- a/Readme.md +++ b/Readme.md @@ -1,3 +1,114 @@ # Secure Boot Objects -This repository is used to hold the secure boot objects recommended by Microsoft such as the KEK, DB, and DBX. +This repository is used to hold the secure boot objects recommended by +Microsoft to use as the default KEK, DB, and DBX variables. This repository +utilizes a script (`scripts/secure_boot_default_keys.py`) to generate the +binary blobs based off of the `keystore.toml` configuration file. + +On a release github release, the script is run and the generated binaries are +bundled, zipped, and attached to the release so that they can be consumed by +platforms. + +## Transparency + +By Keeping the contents of the KeK, Db, and Dbx in a human readable form in +this repository, it enables developers to easily review the contents and make +changes as needed. This also enables an easy way for the KeK, Db, and (mainly) +the Dbx to be updated transparently and then consumed by any platform! + +## Platform Consumption + +The secure boot binary objects are formatted to the expected EDKII data +structures to enable simple integration into any platform. Please refer to +[SecureBootKeyStoreLibOem](https://github.com/microsoft/mu_oem_sample/tree/release/202302/OemPkg/Library/SecureBootKeyStoreLibOem) +to see one example on how to easily integrate these binaries into your +platform. The EDKII build system even supports easily appending to the default +values suggested by Microsoft! + +This is just one suggested way of consuming the binary objects. There are of +course plenty of others, such as storing them as a Freeform Ffs file in an +FV. + +## secure_boot_default_keys.py + +This script ingests a configuration file and generates binary blobs for each +table specified in the configuration file. + +``` cmd +usage: secure_boot_default_keys.py [-h] --keystore KEYSTORE [-o OUTPUT] + +Build the default keys for secure boot. + +options: + -h, --help show this help message and exit + --keystore KEYSTORE A json file containing the keys mapped to certificates and + hashes. + -o OUTPUT, --output OUTPUT + The output directory for the default keys. +``` + +## Configuration File + +A configuration file must be provided to the script to generate the binary +information. The script generates a binary blob for each table entry in the +toml file (a table is each `[]` in the toml file) and for each supported +architecture (Currently Intel, ARM). We also generate a readme for each +architecture to provide information about the files inside each binary blob + +That is to say, if you define a toml file similar to this: + +```toml +[MyCustomKek] + +[MyCustomDb] + +[MyCustomDbx] +``` + +Binary blobs will be created with the following folder structure: + +``` cmd +Artifacts +├── Aarch64 +│ ├── MyCustomKek.bin +│ ├── MyCustomDb.bin +│ ├── MyCustomDbx.bin +│ └── README.md +├── Arm +│ ├── MyCustomKek.bin +│ ├── MyCustomDb.bin +│ ├── MyCustomDbx.bin +│ └── README.md +├── Ia32 +│ ├── MyCustomKek.bin +│ ├── MyCustomDb.bin +│ ├── MyCustomDbx.bin +│ └── README.md +└── X64 + ├── MyCustomKek.bin + ├── MyCustomDb.bin + ├── MyCustomDbx.bin + └── README.md +``` + +For each table in the toml file, the script supports the following entries: + +1. `help (Optional)`: A short blob of information to be added to the + readme for that table entry. +2. `arch (Optional)`: The architecture (Intel, ARM) the blob should be + generated for. Defaults to all. +3. `file (list)`: A list of files to include in the binary blob (.crt, + .csv). This has additional config described below +4. `signature_owner (Optional)`: The GUID of the signature owner. + +For each file in the toml file, the script supports the following entries: + +1. `path (str)`: The local path to the file to include in the binary +2. `url (Optional)`: The url to where the file was downloaded from. + Included in the readme if provided +3. `sha1 (Optional)`: The sha1 hash of the file. Included in the readme + if provided. + +## TODO + +1. make `path` optional and add support for downloading the file from the url. diff --git a/keystore/Db/MicCorUEFCA2011_2011-06-27.crt b/keystore/Db/MicCorUEFCA2011_2011-06-27.crt new file mode 100644 index 0000000000000000000000000000000000000000..9aa6ac6c79b21cf1c23b01b5a747aea6ca15da37 GIT binary patch literal 1556 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 int: + """Entry point for the script.""" + parser = argparse.ArgumentParser( + description="Organizes and zips the files for a release.") + parser.add_argument("input", type=pathlib.Path, + help="The directory containing the files to be Prepared.") + parser.add_argument("--version", required=True, + help="The version number of the release.") + parser.add_argument("-o","--output", default="Archives", type = pathlib.Path, + help="The output directory for prepared files.") + args = parser.parse_args() + + out_path = args.output + in_path = args.input + # Make directory if it doesn't exist. Delete any files in it if it does. + out_path.mkdir(parents=True, exist_ok=True) + for file_path in out_path.rglob("*"): + if file_path.is_file(): + file_path.unlink() + + for key, value in LAYOUT.items(): + tmp_dir = tempfile.TemporaryDirectory() + pathlib.Path(tmp_dir.name, "version").write_text(args.version) + for arch in value: + if not (in_path / arch).exists(): + raise RuntimeError(f"Missing {arch} directory in {in_path}") + shutil.copytree(in_path / arch, pathlib.Path(tmp_dir.name, arch)) + + shutil.make_archive(out_path / key, "zip", tmp_dir.name) + shutil.make_archive(out_path / key, "gztar", tmp_dir.name) + + +if __name__ == "__main__": + + logging.basicConfig(level=logging.INFO, + format="%(levelname)s: %(message)s") + sys.exit(main()) diff --git a/scripts/secure_boot_default_keys.py b/scripts/secure_boot_default_keys.py new file mode 100644 index 0000000..8478e69 --- /dev/null +++ b/scripts/secure_boot_default_keys.py @@ -0,0 +1,324 @@ +# @file +# +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: BSD-2-Clause-Patent +## +"""A command line script used to build the authenticated variable structures for Secureboot.""" +import base64 +import csv +import logging +import uuid +from pathlib import Path +from tempfile import TemporaryFile +from typing import Union + +from edk2toollib.uefi.authenticated_variables_structure_support import ( + EfiSignatureDataEfiCertSha256, + EfiSignatureDataFactory, + EfiSignatureList, +) + +DEFAULT_MS_SIGNATURE_GUID = "77fa9abd-0359-4d32-bd60-28f4e78f784b" +ARCH_MAP = { + "64-bit": "x64", + "32-bit": "ia32", + "32-bit ARM": "arm", + "64-bit ARM": "aarch64" +} + +def _is_pem_encoded(certificate_data: Union[str, bytes]) -> bool: + """This function is used to check if a certificate is pem encoded (base64 encoded). + + Args: + certificate_data (str | bytes): The certificate to check. + + Returns: + bool: True if the certificate is pem encoded, False otherwise. + """ + try: + if isinstance(certificate_data, str): + # If there's any unicode here, an exception will be thrown and the function will return false + sb_bytes = bytes(certificate_data, 'ascii') + elif isinstance(certificate_data, bytes): + sb_bytes = certificate_data + else: + raise ValueError("Argument must be string or bytes") + + return base64.b64encode(base64.b64decode(sb_bytes)) == sb_bytes + except Exception: + return False + +def _convert_pem_to_der(certificate_data: Union[str, bytes]) -> bytes: + """This function is used to convert a pem encoded certificate to a der encoded certificate. + + Args: + certificate_data: The certificate to convert. + + Returns: + bytes: The der encoded certificate. + """ + if isinstance(certificate_data, str): + # If there's any unicode here, an exception will be thrown and the function will return false + certificate_data = bytes(certificate_data, 'ascii') + + return base64.b64decode(certificate_data) + +def _invalid_file(file: str, **kwargs: any) -> None: + """This function is used to handle invalid filetypes. + + Args: + file: The path to the file + + Optional Args: + **kwargs: Additional arguments to be passed to the function (These will be intentionally ignored) + + Raises: + ValueError: If the file is invalid, raise a ValueError. + """ + raise ValueError(f"Invalid filetype for conversion: {file}") + + +def _convert_crt_to_signature_list(file: str, signature_owner: str=DEFAULT_MS_SIGNATURE_GUID, **kwargs: any) -> bytes: + """This function converts a single crt file to a signature list. + + Args: + file: The path to the crt file + signature_owner: The signature owner. Defaults to DEFAULT_MS_SIGNATURE_GUID. + + Optional Args: + **kwargs: Additional arguments to be passed to the function (These will be intentionally ignored) + + Returns: + bytes: The signature list + """ + if signature_owner is not None and not isinstance(signature_owner, uuid.UUID): + signature_owner = uuid.UUID(signature_owner) + + siglist = EfiSignatureList( + typeguid=EfiSignatureDataFactory.EFI_CERT_X509_GUID) + + with open(file, "rb") as crt_file, TemporaryFile() as temp_file: + + certificate = crt_file.read() + if _is_pem_encoded(certificate): + certificate = _convert_pem_to_der(certificate) + + temp_file.write(certificate) + temp_file.seek(0) + + sigdata = EfiSignatureDataFactory.create( + EfiSignatureDataFactory.EFI_CERT_X509_GUID, + temp_file, + signature_owner) + + # X.509 certificates are variable size, so they must be contained in their own signature list + siglist.AddSignatureHeader(None, SigSize=sigdata.get_total_size()) + siglist.AddSignatureData(sigdata) + + return siglist.encode() + + +def _convert_csv_to_signature_list( + file: str, + signature_owner: str=DEFAULT_MS_SIGNATURE_GUID, + target_arch:str=None, + **kwargs: any +) -> bytes: + """This function is used to handle the csv files. + + This function expects to be given a csv file with the following format: + SHA 256 FLAT, PE256 Authenticode, filename, Architecture, Partner, CVEs, Revocation List Date + + This file may be found on uefi.org/revocationlistfile + + Args: + file: The path to the crt file + signature_owner: The signature owner. Defaults to DEFAULT_MS_SIGNATURE_GUID. + target_arch: the arch to filter on when parsing the csv + + Optional Args: + **kwargs: Additional arguments to be passed to the function (These will be intentionally ignored) + + Returns: + bytes: The signature list + """ + if signature_owner is not None and not isinstance(signature_owner, uuid.UUID): + signature_owner = uuid.UUID(signature_owner) + + siglist = EfiSignatureList( + typeguid=EfiSignatureDataFactory.EFI_CERT_SHA256_GUID) + + with open(file, "r") as csv_file: + csv_reader = csv.reader(csv_file, delimiter=",") + for i, row in enumerate(csv_reader): + if i == 0: + siglist.AddSignatureHeader( + None, SigSize=EfiSignatureDataEfiCertSha256.STATIC_STRUCT_SIZE) + continue + + authenticode_hash = row[1] + architecture = ARCH_MAP.get(row[3], None) + + if architecture is None: + raise ValueError(f"Invalid architecture: {architecture}") + if target_arch is not None and architecture != target_arch: + logging.debug(f"Skipping {architecture} because it is not in the target architectures.") + continue + + sigdata = EfiSignatureDataEfiCertSha256( + None, None, bytearray.fromhex(authenticode_hash), sigowner = signature_owner) + siglist.AddSignatureData(sigdata) + + return siglist.encode() + + +def build_default_keys(keystore: dict) -> dict: + """This function is used to build the default keys for secure boot. + + Args: + keystore: A [variable, arch] keyed dictionary containing the matching file in hex format + + Returns: + a dictionary containing the hex representation of the file + + """ + logging.info("Building default keys for secure boot.") + + default_keys = {} + + # Add handlers here for different file types. + file_handler = { + ".crt": _convert_crt_to_signature_list, + '.csv': _convert_csv_to_signature_list + } + + # The json file should be a list of signatures including the owner of the signature. + for variable in keystore: + for arch in set(ARCH_MAP.values()): + + # Skip generating this blob if arch is specified and it does not match + if keystore[variable].get("arch", arch) != arch: + logging.debug(f"Skipping {variable} for {arch} due to config file settings.") + continue + + # The signature database is a byte array that will be added to the default keys. + signature_database = bytes() + + signature_owner = keystore[variable].get( + "signature_owner", "77fa9abd-0359-4d32-60bd-28f4e78f784b") + files = keystore[variable]["files"] + # The files should be handled differently depending on the file extension. + for file_dict in files: + # Get the file extension.\ + file_path = Path(file_dict["path"]) + file_ext = file_path.suffix.lower() + + convert_handler = file_handler.get(file_ext, _invalid_file) + + logging.info("Converting %s to signature list.", file_path) + + signature_database += convert_handler( + file=file_path, + signature_owner=signature_owner, + target_arch=arch + ) + + logging.info( + "Appended %s to signature database for variable %s.", file_path, variable) + + default_keys[arch, variable] = signature_database + + logging.debug("Signature Database for %s:", variable) + + return default_keys + +def create_readme(keystore: dict, arch: str) -> str: + """Generates a README.md file for a given architecture. + + Args: + keystore: A dictionary containing the keys mapped to certificates and hashes. + arch: The architecture to filter on when creating the readme + + Returns: + a string representing the readme. + """ + readme = f"""# {arch.capitalize()} Secure Boot Defaults + +This external dependency contains the default values suggested by microsoft the KEK, DB, and DBX UEFI variables. + +1. The KEK (Key Exchange Key) is a list of certificates that verify the signature of other keys attempting to update the DB and DBX. +2. The DB (Signature Database) is a list of certificates that verify the signature of a binary attempting to execute on the system. +3. The DBX (Forbidden Signature Database) is a list of signatures that are forbidden from executing on the system. + +Please review [Microsoft's documentation](https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/windows-secure-boot-key-creation-and-management-guidance?view=windows-11#15-keys-required-for-secure-boot-on-all-pcs) +for more information on key requirements if appending to the defaults provided in this external dependency. +""" # noqa: E501 + for key, value in keystore.items(): + + # Filter out Tables not used for the specific architecture + if keystore[key].get("arch", arch) != arch: + logging.debug(f"Skipping {key} for {arch} due to config file settings.") + continue + readme += f"\n## {key}\n\n" + + if value.get("help", ""): + readme += f"{value.get('help', '')}\n\n" + readme += "Files Included:\n\n" + + for file_dict in value["files"]: + # Filter out Files not used for the specific architecture + if file_dict.get("arch", arch) != arch: + continue + if file_dict.get("url", None) is not None: + readme += f"* <{file_dict['url']}>\n" + return bytes(readme, "utf-8") + +def main() -> int: + """Main entry point into the tool.""" + import argparse + import pathlib + + import tomllib + + parser = argparse.ArgumentParser( + description="Build the default keys for secure boot.") + parser.add_argument("--keystore", help="A json file containing the keys mapped to certificates and hashes.", + default="keystore.toml", required=True) + parser.add_argument("-o", "--output", type=pathlib.Path, default=pathlib.Path.cwd() / "Artifacts", + help="The output directory for the default keys.") + + args = parser.parse_args() + + with open(args.keystore, "rb") as f: + keystore = tomllib.load(f) + + # Build the default key binaries; filters on requested architectures in the configuration file. + default_keys = build_default_keys(keystore) + # Write the keys to the output directory and create a README.md file for each architecture. + for key, value in default_keys.items(): + arch, variable = key + + out_dir = Path(args.output, arch.capitalize()) + + out_dir.mkdir(exist_ok=True, parents=True) + out_dir.touch() + + out_file = Path(out_dir, f"{variable}.bin") + if out_file.exists(): + out_file.unlink() + with open(out_file, "wb") as f: + f.write(value) + + readme_path = Path(out_dir, "README.md") + if readme_path.exists(): + readme_path.unlink() + with open(readme_path, "wb") as f: + f.write(create_readme(keystore, arch)) + return 0 + + +if __name__ == "__main__": + import sys + logging.basicConfig(level=logging.INFO, + format="%(levelname)s: %(message)s") + sys.exit(main()) From 5aa2ecce6354418ff37d84943a0ffb55f2631d2f Mon Sep 17 00:00:00 2001 From: Joey Vagedes Date: Thu, 17 Aug 2023 09:13:48 -0700 Subject: [PATCH 09/12] Switch to pull_request_target for security purposes --- .github/workflows/prepare-binaries.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prepare-binaries.yml b/.github/workflows/prepare-binaries.yml index 2cccd20..3e76128 100644 --- a/.github/workflows/prepare-binaries.yml +++ b/.github/workflows/prepare-binaries.yml @@ -15,7 +15,7 @@ name: Prepare Secure Boot Binaries on: push: branches: [ "main" ] - pull_request: + pull_request_target: branches: [ "main" ] release: types: [published] From f255be6a15714640b0debc2aa50d719feb05fe78 Mon Sep 17 00:00:00 2001 From: Joey Vagedes Date: Thu, 17 Aug 2023 10:21:26 -0700 Subject: [PATCH 10/12] Update 3PDb to use correct certs --- keystore/keystore.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keystore/keystore.toml b/keystore/keystore.toml index fc00b15..0360da2 100644 --- a/keystore/keystore.toml +++ b/keystore/keystore.toml @@ -59,7 +59,7 @@ url = "https://go.microsoft.com/fwlink/p/?linkid=321194" sha1 = 0x46def63b5ce61cf8ba0de2e6639c1019d0ed14f3 [[Default3PDb.files]] -path = "keystore/Db/windows uefi ca 2023.crt" +path = "keystore/Db/microsoft uefi ca 2023.crt" url = "https://go.microsoft.com/fwlink/?linkid=2239872" sha1 = 0xb5eeb4a6706048073f0ed296e7f580a790b59eaa From f6b6bc35e74f446f0d2c91dec08d6e07ef5dfc65 Mon Sep 17 00:00:00 2001 From: Joey Vagedes Date: Fri, 15 Sep 2023 10:42:43 -0700 Subject: [PATCH 11/12] Update to have different archives per arch rather than per family --- scripts/prepare.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/scripts/prepare.py b/scripts/prepare.py index c9040b8..ed1c963 100644 --- a/scripts/prepare.py +++ b/scripts/prepare.py @@ -12,8 +12,10 @@ import tempfile LAYOUT = { - "edk2-arm-secureboot-binaries": ["Aarch64", "Arm"], - "edk2-intel-secureboot-binaries": ["Ia32", "X64"], + "edk2-arm-secureboot-binaries": "Arm", + "edk2-aarch64-secureboot-binaries": "Aarch64", + "edk2-ia32-secureboot-binaries": "Ia32", + "edk2-x64-secureboot-binaries": "X64", } def main() -> int: @@ -36,16 +38,15 @@ def main() -> int: if file_path.is_file(): file_path.unlink() - for key, value in LAYOUT.items(): + for name, arch in LAYOUT.items(): tmp_dir = tempfile.TemporaryDirectory() pathlib.Path(tmp_dir.name, "version").write_text(args.version) - for arch in value: - if not (in_path / arch).exists(): - raise RuntimeError(f"Missing {arch} directory in {in_path}") - shutil.copytree(in_path / arch, pathlib.Path(tmp_dir.name, arch)) + if not (in_path / arch).exists(): + raise RuntimeError(f"Missing {arch} directory in {in_path}") + shutil.copytree(in_path / arch, pathlib.Path(tmp_dir.name), dirs_exist_ok=True) - shutil.make_archive(out_path / key, "zip", tmp_dir.name) - shutil.make_archive(out_path / key, "gztar", tmp_dir.name) + shutil.make_archive(out_path / name, "zip", tmp_dir.name) + shutil.make_archive(out_path / name, "gztar", tmp_dir.name) if __name__ == "__main__": From 3102120cd4771e99f9720520143d02b4c511fe7b Mon Sep 17 00:00:00 2001 From: Joey Vagedes Date: Fri, 15 Sep 2023 10:43:47 -0700 Subject: [PATCH 12/12] update --- Readme.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Readme.md b/Readme.md index 883e950..cfb9a31 100644 --- a/Readme.md +++ b/Readme.md @@ -108,7 +108,3 @@ For each file in the toml file, the script supports the following entries: Included in the readme if provided 3. `sha1 (Optional)`: The sha1 hash of the file. Included in the readme if provided. - -## TODO - -1. make `path` optional and add support for downloading the file from the url.