From a2243609abc0a273e28fdff0807750b9900ae77c Mon Sep 17 00:00:00 2001 From: Doug Flick Date: Mon, 7 Jul 2025 15:15:40 -0700 Subject: [PATCH 1/6] Initial commit of debug tooling --- scripts/auth_var_tool.py | 222 +++++++++++++++++++++++++ scripts/windows/CreateTestCerts.ps1 | 249 ++++++++++++++++++++++++++++ 2 files changed, 471 insertions(+) create mode 100644 scripts/auth_var_tool.py create mode 100644 scripts/windows/CreateTestCerts.ps1 diff --git a/scripts/auth_var_tool.py b/scripts/auth_var_tool.py new file mode 100644 index 0000000..6814259 --- /dev/null +++ b/scripts/auth_var_tool.py @@ -0,0 +1,222 @@ +"""Signs a variable in accordance with EFI_AUTHENTICATION_2. + +Relevant RFC's + * (PKCS #7: Cryptographic Message Syntax)[https://www.rfc-editor.org/rfc/rfc2315] + * (Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile + (In particular To-be-signed Certificate)) + [https://www.rfc-editor.org/rfc/rfc5280#section-4.1.2] + * https://www.itu.int/ITU-T/formal-language/itu-t/x/x420/1999/PKCS7.html + +# TODO: + * Implement Certificate Verification (https://stackoverflow.com/questions/70654598/python-pkcs7-x509-chain-of-trust-with-cryptography) + +pip requirements: + pyasn1 + pyasn1_modules + edk2toollib + cryptography # Depends on having openssl installed +""" + +import argparse +import logging +import os +import sys +import uuid +from getpass import getpass + +from cryptography.hazmat.primitives.serialization import pkcs12 +from edk2toollib.uefi.authenticated_variables_structure_support import ( + EfiVariableAuthentication2, + EfiVariableAuthentication2Builder, +) + +# from edk2toollib.uefi.uefi_multi_phase import EfiVariableAttributes + +# Puts the script into debug mode, may be enabled via argparse +ENABLE_DEBUG = False + +# Index into the certificate argument +CERTIFICATE_FILE_PATH = 0 +CERTIFICATE_PASSWORD = 1 + + +logging.basicConfig() +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +def sign_variable(args: argparse.Namespace) -> int: + """Signs a variable in accordance with EFI_AUTHENTICATION_2 using the provided arguments. + + Parameters + ---------- + args : argparse.Namespace + The parsed command-line arguments required for signing the variable. + + Returns: + ------- + int + Status code (0 for success, non-zero for failure). + """ + with open(args.data_file, 'rb') as f: + data = f.read() + + builder = EfiVariableAuthentication2Builder( + name=args.name, + guid=args.guid, + attributes=args.attributes, + payload=data, + ) + + # Load the signing certificate from the PFX file + with open(args.pfx_file, 'rb') as f: + password = getpass("Enter the password for the PFX file: ").encode('utf-8') + pkcs12_store = pkcs12.load_pkcs12( + f.read(), + password + ) + + builder.sign(pkcs12_store.cert.certificate, pkcs12_store.key) + + auth_var = builder.finalize() + + name = args.name + logger.info(f"Signing variable: {name} with GUID: {args.guid}") + output_file = os.path.join(args.output_dir, f"{name}.authvar.bin") + + with open(output_file, "wb") as f: + f.write(auth_var.encode()) + + logger.info(f"Signed variable saved to: {output_file}") + +def describe_variable(args: argparse.Namespace) -> int: + auth_var = None + with open(args.signed_payload, 'rb') as f: + auth_var = EfiVariableAuthentication2(decodefs=f) + + name = os.path.basename(args.signed_payload) + output_file = os.path.join(args.output_dir, f"{name}.authvar.txt") + + with open(output_file, 'w') as f: + auth_var.print(outfs=f) + + logger.info(f"Output: {output_file}") + + return 0 + + +def typecheck_file_exists(filepath: str) -> str: + """Checks if this is a valid filepath for argparse. + + :param filepath: filepath to check for existance + + :return: valid filepath + """ + if not os.path.isfile(filepath): + raise argparse.ArgumentTypeError( + f"You sure this is a valid filepath? : {filepath}") + + return filepath + +def setup_sign_parser(subparsers: argparse._SubParsersAction) -> argparse._SubParsersAction: + """Sets up the sign parser. + + :param subparsers: - sub parser from argparse to add options to + + :returns: subparser + """ + sign_parser = subparsers.add_parser( + "sign", help="Signs variables using the command line" + ) + sign_parser.set_defaults(function=sign_variable) + + sign_parser.add_argument( + "name", + help="UTF16 Formated Name of Variable" + ) + + sign_parser.add_argument( + "guid", type=uuid.UUID, + help="UUID of the namespace the variable belongs to. (Ex. 12345678-1234-1234-1234-123456789abc)" + ) + + sign_parser.add_argument( + "attributes", + help="Variable Attributes, AT is a required attribute (Ex. \"NV,BT,RT,AT\")" + ) + + sign_parser.add_argument( + "data_file", type=typecheck_file_exists, + help="Binary file of variable data. An empty file is accepted and will be used to clear the authenticated data" + ) + + sign_parser.add_argument( + "pfx_file", type=typecheck_file_exists, + help="Pkcs12 certificate to sign the authenticated data with (Cert.pfx)" + ) + + sign_parser.add_argument( + "--output-dir", default="./", + help="Output directory for the signed data" + ) + + return subparsers + + +def setup_describe_parser(subparsers): + + describe_parser = subparsers.add_parser( + "describe", help="Parses Authenticated Variable 2 structures" + ) + describe_parser.set_defaults(function=describe_variable) + + describe_parser.add_argument( + "signed_payload", type=typecheck_file_exists, + help="Signed payload to parse" + ) + + describe_parser.add_argument( + "--output-dir", default="./", + help="Output directory for the described data" + ) + + return subparsers + + +def parse_args(): + """Parses arguments from the command line + """ + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers() + + parser.add_argument( + "--debug", action='store_true', default=False, + help="enables debug printing for deep inspection" + ) + + subparsers = setup_sign_parser(subparsers) + subparsers = setup_describe_parser(subparsers) + + args = parser.parse_args() + + if not hasattr(args, "function"): + parser.print_help(sys.stderr) + sys.exit(1) + + global ENABLE_DEBUG + ENABLE_DEBUG = args.debug + + if ENABLE_DEBUG: + logger.setLevel(logging.DEBUG) + + return args + + +def main(): + args = parse_args() + + status_code = args.function(args) + + return sys.exit(status_code) + + +main() diff --git a/scripts/windows/CreateTestCerts.ps1 b/scripts/windows/CreateTestCerts.ps1 new file mode 100644 index 0000000..514ed40 --- /dev/null +++ b/scripts/windows/CreateTestCerts.ps1 @@ -0,0 +1,249 @@ +#Requires -RunAsAdministrator + +# This script generates test certificates for use with Secure Boot testing. + +param ( + [string]$Action +) + +Write-Warning @" +================================================================================ +WARNING: This script is for validation and testing purposes only. +DO NOT use the generated certificates or keys in production environments. +Keys are stored in software and are not protected by a Hardware Security Module (HSM). +For production, always use an HSM or other secure key storage solution. +================================================================================ +"@ + +# ============================================================================= +# Script Variables - Do not change +# ============================================================================= +# Change these variables to your own values if required + +# This is the text that will be appended to the end of the certificate's OU field +$AdditionalText = "TESTING ONLY - DO NOT USE FOR PRODUCTION" + + + +if (-not $Env:TestSecureBootDefaults) { + $Env:TestSecureBootDefaults = (Get-Location).Path + "\SecureBootDefaults" +} + +$DbOutputDir = "$($Env:TestSecureBootDefaults)\DB" +$KEKOutputDir = "$($Env:TestSecureBootDefaults)\KEK" +$PKOutputDir = "$($Env:TestSecureBootDefaults)\PK" + +# This is the directory where the certificates will be created +$SigningCerts = "$($Env:TestSecureBootDefaults)\SigningCerts" + +$TestPKName = "TestPK" +$TestKEKName = "TestKEK" +$TestDBName = "TestDB" + +# Create a list of the certificates to create +$CertsToCreate = @( + # The PK certificate + @{ + Subject = "CN=$TestPKName OU=$AdditionalText" + Name = $TestPKName + CrtPath = "$PKOutputDir\$TestPKName.crt" # The CRT file is the public key used by the UEFI firmware (in DER format) + PfxPath = "$SigningCerts\$TestPKName.pfx" # The PFX file is the private key used by the signing tools + P7bPath = "$SigningCerts\$TestPKName.p7b" # The P7B file is the PKCS#7 file that contains the certificate chain used by commands to verify the signature + OutputDir = $PKOutputDir + }, + # The KEK certificate + @{ + Subject = "CN=$TestKEKName OU=$AdditionalText" + Name = $TestKEKName + CrtPath = "$KEKOutputDir\$TestKEKName.crt" + PfxPath = "$SigningCerts\$TestKEKName.pfx" + P7bPath = "$SigningCerts\$TestKEKName.p7b" + OutputDir = $KEKOutputDir + }, + # The DB certificate + @{ + Subject = "CN=$TestDBName OU=$AdditionalText" + Name = $TestDBName + CrtPath = "$DbOutputDir\$TestDBName.crt" + PfxPath = "$SigningCerts\$TestDBName.pfx" + P7bPath = "$SigningCerts\$TestDBName.p7b" + OutputDir = $DbOutputDir + } +) + +$CertStore = "Cert:\LocalMachine\My" + +# These are the common parameters that will be used to create the certificates +$CommonParams = @{ + Type = "Custom" + KeyUsage = "DigitalSignature" + KeyAlgorithm = "RSA" + KeyLength = 2048 # 2048 is the minimum key length for Secure Boot + KeyExportPolicy = "Exportable" + CertStoreLocation = $CertStore + NotAfter = (Get-Date).AddYears(1) +} + +# ============================================================================= +# Script Functions +# ============================================================================= + +function Test-Action { + param ( + [string]$Action, + [array]$ValidActions + ) + + if (-not $Action) { + Write-Host "Please provide an action ($ValidActions) using the -Action parameter." -ForegroundColor Yellow + return $false + } + + if ($ValidActions -notcontains $Action) { + Write-Host "Invalid action. Supported actions are $ValidActions." -ForegroundColor Red + return $false + } + + return $true +} + +# Create a function to generate a new certificate +function New-Certificate +{ + param ( + [Parameter(Mandatory=$true)] + [string]$Subject, + [Parameter(Mandatory=$true)] + [string]$Name, + [Parameter(Mandatory=$true)] + [string]$CrtPath, + [Parameter(Mandatory=$true)] + [string]$PfxPath, + [Parameter(Mandatory=$true)] + [string]$P7bPath + ) + write-host $CrtPath + + $Cert = New-SelfSignedCertificate -Subject $Subject @CommonParams + $Cert | Export-Certificate -FilePath $CrtPath -Type CERT + $Cert | Export-Certificate -FilePath $P7bPath -Type p7b + + + Export-PfxCertificate -Cert "$CertStore\$($Cert.Thumbprint)" -FilePath $PfxPath -Password $CertPassword +} + +# create a function that deletes the certificates from the local machine +function Delete-Certificate +{ + param ( + [Parameter(Mandatory=$true)] + [string]$Subject + ) + + # Loop over Cert:\LocalMachine\My and delete any certificate with "Test" in the common name + + $certStore = Get-ChildItem -Path Cert:\LocalMachine\My | Where-Object { $_.Subject -match "CN=.*$Subject" } + + if ($certStore.Count -eq 0) { + Write-Host "No certificates with 'Test' in the common name found." + return + } + + foreach ($cert in $certStore) { + $commonName = $cert.Subject -replace '^.*?CN=([^,]*).*$', '$1' + Write-Host "Removing certificate with common name: $commonName" + Remove-Item $cert.PSPath -Force + } + + Write-Host "Certificates with '$Subject' in the common name removed successfully." +} + +function New-OutputDirIfNotExists { + param ( + [Parameter(Mandatory=$true)] + [string]$OutputDir + ) + + if (-not (Test-Path $OutputDir)) { + New-Item -Path $OutputDir -ItemType Directory + } +} + +function Remove-OutputDirIfExists { + param ( + [Parameter(Mandatory=$true)] + [string]$OutputDir + ) + + if (Test-Path $OutputDir) { + Remove-Item -Path $OutputDir -Recurse -Force + } +} + +# ============================================================================= +# Script Execution +# ============================================================================= + +if (-not (Test-Action -Action $Action -ValidActions @("create", "delete"))) { + return +} + +if ($Action.equals("create")) { + + # Create the output directories for the signing certificates + New-OutputDirIfNotExists $SigningCerts + # Prompt the user to enter a password for the certificates + $CertPassword = Read-Host -AsSecureString -Prompt "Enter a password to protect the certificate private keys" + Set-Variable -Name CertPassword -Value $CertPassword -Scope Global + + # for each of the certificates to create, call New-Certificate + foreach ($cert in $CertsToCreate) { + if (Test-Path $cert.CrtPath) { + Write-Host "Test certificate $($cert.Name) already exists. Delete it first using the -Action delete parameter." -ForegroundColor Yellow + return + } + + New-OutputDirIfNotExists $cert.OutputDir + + Write-Host "Creating test certificate $($cert.Name): $($cert.CrtPath)" + + New-Certificate -Subject $cert.Subject -Name $cert.Name -CrtPath $cert.CrtPath -PfxPath $cert.PfxPath -P7bPath $cert.P7bPath + + } + + write-host "Use the *.crt files for the UEFI Secure Boot Setup" + + + +} elseif ($Action.Equals("delete")) { + + # for each certificate created + foreach ($cert in $CertsToCreate) { + + # test if the OutputDir exists + If (Test-Path $cert.OutputDir) { + + # if it does, delete it + Remove-OutputDirIfExists $cert.OutputDir + } + + # test if the certificate exists + If (Test-Path $cert.CrtPath) { + + # if it does, delete it + Remove-Item -Path $cert.CrtPath -Force + } + + # test if the pfx file exists + If (Test-Path $cert.PfxPath) { + + # if it does, delete it + Remove-Item -Path $cert.PfxPath -Force + } + + Delete-Certificate -Subject $cert.Name + + } + + write-host "Deleted test certificates" +} From d0b4177ea7ce23609d777d2b01547e9710bd2393 Mon Sep 17 00:00:00 2001 From: Doug Flick Date: Wed, 9 Jul 2025 01:12:28 -0700 Subject: [PATCH 2/6] clean up docstrings, and user strings, and copyrights --- scripts/auth_var_tool.py | 54 ++++++++++++++++++----------- scripts/windows/CreateTestCerts.ps1 | 22 +++++++----- 2 files changed, 47 insertions(+), 29 deletions(-) diff --git a/scripts/auth_var_tool.py b/scripts/auth_var_tool.py index 6814259..62257a3 100644 --- a/scripts/auth_var_tool.py +++ b/scripts/auth_var_tool.py @@ -1,3 +1,8 @@ +# @file +# +# Copyright (c) Microsoft Corporation +# SPDX-License-Identifier: BSD-2-Clause-Patent +## """Signs a variable in accordance with EFI_AUTHENTICATION_2. Relevant RFC's @@ -6,15 +11,6 @@ (In particular To-be-signed Certificate)) [https://www.rfc-editor.org/rfc/rfc5280#section-4.1.2] * https://www.itu.int/ITU-T/formal-language/itu-t/x/x420/1999/PKCS7.html - -# TODO: - * Implement Certificate Verification (https://stackoverflow.com/questions/70654598/python-pkcs7-x509-chain-of-trust-with-cryptography) - -pip requirements: - pyasn1 - pyasn1_modules - edk2toollib - cryptography # Depends on having openssl installed """ import argparse @@ -30,8 +26,6 @@ EfiVariableAuthentication2Builder, ) -# from edk2toollib.uefi.uefi_multi_phase import EfiVariableAttributes - # Puts the script into debug mode, may be enabled via argparse ENABLE_DEBUG = False @@ -39,7 +33,6 @@ CERTIFICATE_FILE_PATH = 0 CERTIFICATE_PASSWORD = 1 - logging.basicConfig() logger = logging.getLogger() logger.setLevel(logging.INFO) @@ -89,6 +82,19 @@ def sign_variable(args: argparse.Namespace) -> int: logger.info(f"Signed variable saved to: {output_file}") def describe_variable(args: argparse.Namespace) -> int: + """Parses and describes an authenticated variable structure. + + Parameters + ---------- + args : argparse.Namespace + The parsed command-line arguments containing the signed payload file path + and output directory. + + Returns: + ------- + int + Status code (0 for success). + """ auth_var = None with open(args.signed_payload, 'rb') as f: auth_var = EfiVariableAuthentication2(decodefs=f) @@ -141,7 +147,7 @@ def setup_sign_parser(subparsers: argparse._SubParsersAction) -> argparse._SubPa sign_parser.add_argument( "attributes", - help="Variable Attributes, AT is a required attribute (Ex. \"NV,BT,RT,AT\")" + help="Variable Attributes, AT is a required attribute (Ex. \"NV,BS,RT,AT\")" ) sign_parser.add_argument( @@ -162,8 +168,13 @@ def setup_sign_parser(subparsers: argparse._SubParsersAction) -> argparse._SubPa return subparsers -def setup_describe_parser(subparsers): +def setup_describe_parser(subparsers: argparse._SubParsersAction) -> argparse._SubParsersAction: + """Sets up the describe parser. + :param subparsers: - sub parser from argparse to add options to + + :returns: subparser + """ describe_parser = subparsers.add_parser( "describe", help="Parses Authenticated Variable 2 structures" ) @@ -181,10 +192,8 @@ def setup_describe_parser(subparsers): return subparsers - -def parse_args(): - """Parses arguments from the command line - """ +def parse_args() -> argparse.Namespace: + """Parses arguments from the command line.""" parser = argparse.ArgumentParser() subparsers = parser.add_subparsers() @@ -211,12 +220,17 @@ def parse_args(): return args -def main(): +def main() -> None: + """Entry point for the auth_var_tool script. + + Parses command-line arguments and executes the appropriate subcommand + (sign or describe) based on user input. + """ args = parse_args() status_code = args.function(args) - return sys.exit(status_code) + sys.exit(status_code) main() diff --git a/scripts/windows/CreateTestCerts.ps1 b/scripts/windows/CreateTestCerts.ps1 index 514ed40..5a54d10 100644 --- a/scripts/windows/CreateTestCerts.ps1 +++ b/scripts/windows/CreateTestCerts.ps1 @@ -1,7 +1,18 @@ #Requires -RunAsAdministrator +<# +.DESCRIPTION # This script generates test certificates for use with Secure Boot testing. +.PARAMETER Action +Specifies the action to perform. Valid values are "create" or "delete". + +.NOTES +Author: Microsoft +Date: 7/8/25 +Version: 1.0 +#> + param ( [string]$Action ) @@ -15,16 +26,9 @@ For production, always use an HSM or other secure key storage solution. ================================================================================ "@ -# ============================================================================= -# Script Variables - Do not change -# ============================================================================= -# Change these variables to your own values if required - # This is the text that will be appended to the end of the certificate's OU field $AdditionalText = "TESTING ONLY - DO NOT USE FOR PRODUCTION" - - if (-not $Env:TestSecureBootDefaults) { $Env:TestSecureBootDefaults = (Get-Location).Path + "\SecureBootDefaults" } @@ -78,7 +82,7 @@ $CommonParams = @{ Type = "Custom" KeyUsage = "DigitalSignature" KeyAlgorithm = "RSA" - KeyLength = 2048 # 2048 is the minimum key length for Secure Boot + KeyLength = 2048 # 2048 is the minimum key length for Secure Boot KeyExportPolicy = "Exportable" CertStoreLocation = $CertStore NotAfter = (Get-Date).AddYears(1) @@ -189,7 +193,7 @@ if (-not (Test-Action -Action $Action -ValidActions @("create", "delete"))) { } if ($Action.equals("create")) { - + # Create the output directories for the signing certificates New-OutputDirIfNotExists $SigningCerts # Prompt the user to enter a password for the certificates From 0cecad16628f55619b52b679acf1cd47a708d4ec Mon Sep 17 00:00:00 2001 From: Doug Flick Date: Wed, 9 Jul 2025 16:13:57 -0700 Subject: [PATCH 3/6] adding functionality to print the efi_signature_list from commandline --- scripts/utility_functions.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/scripts/utility_functions.py b/scripts/utility_functions.py index c269a35..0a9c15f 100644 --- a/scripts/utility_functions.py +++ b/scripts/utility_functions.py @@ -8,9 +8,12 @@ including SVN data, signature databases, and secure boot payloads. """ +import argparse import hashlib +import json import pathlib import struct +import sys from dataclasses import dataclass from uuid import UUID @@ -286,3 +289,35 @@ def get_unsigned_payload_receipt(efi_sig_database: pathlib.Path) -> dict: receipt["signatureDatabase"] = readable_signature_database return receipt + +if __name__ == "__main__": + + parser = argparse.ArgumentParser(description="Utility for parsing and describing secure boot signature databases") + parser.add_argument("file", type=pathlib.Path, help="Path to the signature database file") + parser.add_argument("--signed", action="store_true", help="Indicates the file is a signed EFI variable") + parser.add_argument("--output", "-o", type=pathlib.Path, help="Output file for the receipt (JSON format)") + + args = parser.parse_args() + + if not args.file.exists(): + print(f"Error: File '{args.file}' not found.", file=sys.stderr) + sys.exit(1) + + try: + if args.signed: + receipt = get_signed_payload_receipt(args.file) + else: + receipt = get_unsigned_payload_receipt(args.file) + + json_output = json.dumps(receipt, indent=2) + + if args.output: + with open(args.output, "w") as f: + f.write(json_output) + print(f"Receipt written to '{args.output}'") + else: + print(json_output) + + except Exception as e: + print(f"Error processing file: {e}", file=sys.stderr) + sys.exit(1) From 1fb827c9af2a68953cdb22f75235f725cb836349 Mon Sep 17 00:00:00 2001 From: Doug Flick Date: Thu, 10 Jul 2025 14:34:26 -0700 Subject: [PATCH 4/6] adding feature to generate signable components --- scripts/auth_var_tool.py | 98 ++++++++++++++++++++++++++++++---------- 1 file changed, 75 insertions(+), 23 deletions(-) diff --git a/scripts/auth_var_tool.py b/scripts/auth_var_tool.py index 62257a3..9ad21ba 100644 --- a/scripts/auth_var_tool.py +++ b/scripts/auth_var_tool.py @@ -50,36 +50,87 @@ def sign_variable(args: argparse.Namespace) -> int: int Status code (0 for success, non-zero for failure). """ + # Read the variable data with open(args.data_file, 'rb') as f: data = f.read() - builder = EfiVariableAuthentication2Builder( - name=args.name, - guid=args.guid, - attributes=args.attributes, - payload=data, - ) + # Create the authentication builder + builder = EfiVariableAuthentication2Builder( + name=args.name, + guid=args.guid, + attributes=args.attributes, + payload=data, + ) + + # Handle case where no PFX file is provided (output signable data) + if not args.pfx_file: + return _create_signable_data(builder, args) + + # Handle case where PFX file is provided (sign the variable) + return _sign_with_pfx(builder, args) + + +def _create_signable_data(builder: EfiVariableAuthentication2Builder, args: argparse.Namespace) -> int: + """Creates signable data when no PFX file is provided. + + Parameters + ---------- + builder : EfiVariableAuthentication2Builder + + args : argparse.Namespace + The parsed command-line arguments containing the output directory and variable name. + + Returns: + ------- + int + Status code (0 for success). + """ + logger.info("No PFX file provided, outputting signable data.") - # Load the signing certificate from the PFX file - with open(args.pfx_file, 'rb') as f: - password = getpass("Enter the password for the PFX file: ").encode('utf-8') - pkcs12_store = pkcs12.load_pkcs12( - f.read(), - password - ) + output_file = os.path.join(args.output_dir, f"{args.name}.signable.bin") - builder.sign(pkcs12_store.cert.certificate, pkcs12_store.key) + with open(output_file, "wb") as f: + f.write(builder.get_digest()) - auth_var = builder.finalize() + logger.info(f"Signable data for {args.name} with GUID: {args.guid}") + logger.info(f"Signable data saved to: {output_file}") + return 0 + + +def _sign_with_pfx(builder: EfiVariableAuthentication2Builder, args: argparse.Namespace) -> int: + """Signs the variable using the provided PFX file. + + Parameters + ---------- + builder : EfiVariableAuthentication2Builder + + args : argparse.Namespace + The parsed command-line arguments containing the PFX file path and output directory. - name = args.name - logger.info(f"Signing variable: {name} with GUID: {args.guid}") - output_file = os.path.join(args.output_dir, f"{name}.authvar.bin") + Returns: + ------- + int + Status code (0 for success). + """ + # Load the signing certificate from the PFX file + with open(args.pfx_file, 'rb') as f: + pfx_data = f.read() + + password = getpass("Enter the password for the PFX file: ").encode('utf-8') + pkcs12_store = pkcs12.load_pkcs12(pfx_data, password) - with open(output_file, "wb") as f: - f.write(auth_var.encode()) + # Sign the variable + builder.sign(pkcs12_store.cert.certificate, pkcs12_store.key) + auth_var = builder.finalize() - logger.info(f"Signed variable saved to: {output_file}") + # Save the signed variable + output_file = os.path.join(args.output_dir, f"{args.name}.authvar.bin") + with open(output_file, "wb") as f: + f.write(auth_var.encode()) + + logger.info(f"Signed variable: {args.name} with GUID: {args.guid}") + logger.info(f"Signed variable saved to: {output_file}") + return 0 def describe_variable(args: argparse.Namespace) -> int: """Parses and describes an authenticated variable structure. @@ -156,8 +207,9 @@ def setup_sign_parser(subparsers: argparse._SubParsersAction) -> argparse._SubPa ) sign_parser.add_argument( - "pfx_file", type=typecheck_file_exists, - help="Pkcs12 certificate to sign the authenticated data with (Cert.pfx)" + "--pfx-file", default=None, + help="Pkcs12 certificate to sign the authenticated data with (Cert.pfx)." \ + " If not provided, outputs the signable data instead." ) sign_parser.add_argument( From 4f432354653a333f62a90e94113bed27bfe1a6ca Mon Sep 17 00:00:00 2001 From: Doug Flick Date: Fri, 11 Jul 2025 09:23:30 -0700 Subject: [PATCH 5/6] Adding a format subcommand to allow offloading a digest to HSM and signing with a HSM --- scripts/auth_var_tool.py | 414 ++++++++++++++++++++++++++++++++++----- 1 file changed, 366 insertions(+), 48 deletions(-) diff --git a/scripts/auth_var_tool.py b/scripts/auth_var_tool.py index 9ad21ba..a005497 100644 --- a/scripts/auth_var_tool.py +++ b/scripts/auth_var_tool.py @@ -3,17 +3,42 @@ # Copyright (c) Microsoft Corporation # SPDX-License-Identifier: BSD-2-Clause-Patent ## -"""Signs a variable in accordance with EFI_AUTHENTICATION_2. +"""UEFI Authenticated Variable Tool for signing and formatting variables. -Relevant RFC's +This tool provides three main commands: + +1. format: Generates signable data and receipt files for external signing workflows +2. sign: Signs variables using PFX files or attaches pre-generated signatures +3. describe: Parses and describes existing signed variables + +The tool supports both direct signing (using PFX files) and external signing +workflows (where signatures are generated outside this tool and then attached). + +Relevant RFC's: * (PKCS #7: Cryptographic Message Syntax)[https://www.rfc-editor.org/rfc/rfc2315] * (Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile (In particular To-be-signed Certificate)) [https://www.rfc-editor.org/rfc/rfc5280#section-4.1.2] * https://www.itu.int/ITU-T/formal-language/itu-t/x/x420/1999/PKCS7.html + +Examples: + # Generate signable data for external signing + python auth_var_tool.py format MyVar 8be4df61-93ca-11d2-aa0d-00e098032b8c "NV,BS,RT,AT" mydata.bin + + # Sign directly with PFX file + python auth_var_tool.py sign MyVar 8be4df61-93ca-11d2-aa0d-00e098032b8c "NV,BS,RT,AT" mydata.bin --pfx-file cert.pfx + + # Attach external signature using receipt + python auth_var_tool.py sign --receipt-file MyVar.receipt.json --signature-file MyVar.bin.p7 + + # Describe an existing signed variable + python auth_var_tool.py describe signed_variable.bin """ import argparse +import datetime +import io +import json import logging import os import sys @@ -37,21 +62,37 @@ logger = logging.getLogger() logger.setLevel(logging.INFO) -def sign_variable(args: argparse.Namespace) -> int: - """Signs a variable in accordance with EFI_AUTHENTICATION_2 using the provided arguments. +def format_variable(args: argparse.Namespace) -> int: + """Formats a variable for signing by generating signable data and a receipt file. + + This command is used to prepare variables for external signing workflows + by generating the signable data and a receipt that can be used later + to attach a pre-generated signature. Parameters ---------- args : argparse.Namespace - The parsed command-line arguments required for signing the variable. + The parsed command-line arguments required for formatting the variable. Returns: ------- int Status code (0 for success, non-zero for failure). """ + # Validate required arguments + required_args = ["name", "guid", "attributes", "data_file"] + missing_args = [arg for arg in required_args if not getattr(args, arg, None)] + if missing_args: + logger.error(f"Missing required arguments: {', '.join(missing_args)}") + return 1 + + # Validate data file exists + if not os.path.isfile(args.data_file): + logger.error(f"Data file not found: {args.data_file}") + return 1 + # Read the variable data - with open(args.data_file, 'rb') as f: + with open(args.data_file, "rb") as f: data = f.read() # Create the authentication builder @@ -62,12 +103,101 @@ def sign_variable(args: argparse.Namespace) -> int: payload=data, ) - # Handle case where no PFX file is provided (output signable data) - if not args.pfx_file: - return _create_signable_data(builder, args) + # Generate signable data and receipt + logger.info(f"Formatting variable '{args.name}' for external signing.") + return _create_signable_data(builder, args) + + +def sign_variable(args: argparse.Namespace) -> int: + """Signs a variable in accordance with EFI_AUTHENTICATION_2 using the provided arguments. + + This command handles two signing workflows: + 1. Direct signing with a PFX file + 2. Attaching a pre-generated signature (with or without receipt) + + Parameters + ---------- + args : argparse.Namespace + The parsed command-line arguments required for signing the variable. - # Handle case where PFX file is provided (sign the variable) - return _sign_with_pfx(builder, args) + Returns: + ------- + int + Status code (0 for success, non-zero for failure). + """ + # Handle receipt-based signature attachment (doesn't need variable args) + if hasattr(args, "receipt_file") and args.receipt_file: + if not args.signature_file: + logger.error("--receipt-file requires --signature-file to be specified.") + return 1 + if args.pfx_file: + logger.error( + "Cannot use --receipt-file with --pfx-file. Receipt mode is for attaching external signatures only." + ) + return 1 + return _attach_signature_from_receipt(args) + + # Validate mutually exclusive options + if args.pfx_file and args.signature_file: + logger.error("Cannot specify both --pfx-file and --signature-file. Choose one signing method.") + return 1 + + # Validate that we have either PFX or signature file + if not args.pfx_file and not args.signature_file: + logger.error("Must specify either --pfx-file or --signature-file for signing.") + logger.error("To generate signable data, use the 'format' command instead.") + return 1 + + # For non-receipt workflows, validate required arguments + if not args.receipt_file: + required_args = ["name", "guid", "attributes", "data_file"] + missing_args = [arg for arg in required_args if not getattr(args, arg, None)] + if missing_args: + logger.error(f"Missing required arguments: {', '.join(missing_args)}") + logger.error("These arguments are required unless using --receipt-file with --signature-file") + return 1 + + # Set timestamp if provided + timestamp = datetime.datetime.now() + if args.timestamp: + try: + # Parse ISO 8601 format timestamp + if "T" in args.timestamp: + provided_time = datetime.datetime.fromisoformat(args.timestamp) + else: + # Support date-only format, default to midnight + provided_time = datetime.datetime.fromisoformat(args.timestamp + "T00:00:00") + + # Ensure timezone-aware (default to UTC if not specified) + if provided_time.tzinfo is None: + provided_time = provided_time.replace(tzinfo=datetime.timezone.utc) + + timestamp = provided_time.astimezone(datetime.timezone.utc) + logger.info(f"Using provided timestamp: {timestamp.isoformat()}") + + except ValueError: + logger.error( + f"Invalid timestamp format: {args.timestamp}. Expected ISO 8601 format (YYYY-MM-DDTHH:MM:SS)" + ) + return 1 + + # Validate data file exists + if not os.path.isfile(args.data_file): + logger.error(f"Data file not found: {args.data_file}") + return 1 + + # Read the variable data + with open(args.data_file, "rb") as f: + data = f.read() + + # Create the authentication builder + builder = EfiVariableAuthentication2Builder( + name=args.name, guid=args.guid, attributes=args.attributes, payload=data, efi_time=timestamp + ) + + # Handle case where PFX file is provided (sign the variable) + if args.pfx_file: + return _sign_with_pfx(builder, args) def _create_signable_data(builder: EfiVariableAuthentication2Builder, args: argparse.Namespace) -> int: @@ -85,15 +215,52 @@ def _create_signable_data(builder: EfiVariableAuthentication2Builder, args: argp int Status code (0 for success). """ - logger.info("No PFX file provided, outputting signable data.") - + # Generate timestamp for the signing operation + if args.timestamp: + try: + # Parse ISO 8601 format timestamp + timestamp_str = args.timestamp if "T" in args.timestamp else args.timestamp + "T00:00:00" + provided_time = datetime.datetime.fromisoformat(timestamp_str) + + # Ensure timezone-aware (default to UTC if not specified) + if provided_time.tzinfo is None: + provided_time = provided_time.replace(tzinfo=datetime.timezone.utc) + + signing_time = provided_time.astimezone(datetime.timezone.utc) + logger.info(f"Using provided timestamp: {signing_time.isoformat()}") + except ValueError: + logger.error(f"Invalid timestamp format: {args.timestamp}. Expected ISO 8601 format (YYYY-MM-DDTHH:MM:SS)") + return 1 + else: + # Use current time + signing_time = datetime.datetime.now(datetime.timezone.utc) + logger.info(f"Using current timestamp: {signing_time.isoformat()}") + + # Create the signable data output file output_file = os.path.join(args.output_dir, f"{args.name}.signable.bin") - with open(output_file, "wb") as f: f.write(builder.get_digest()) + # Create a receipt file with all the metadata needed for signature attachment + receipt_data = { + "variable_name": args.name, + "variable_guid": str(args.guid), + "variable_attributes": args.attributes, + "data_file": os.path.abspath(args.data_file), + "signing_timestamp": signing_time.isoformat(), + "signable_data_file": os.path.abspath(output_file), + "tool_version": "1.0", + "created": datetime.datetime.now(datetime.timezone.utc).isoformat(), + } + + receipt_file = os.path.join(args.output_dir, f"{args.name}.receipt.json") + with open(receipt_file, "w") as f: + json.dump(receipt_data, f, indent=2) + logger.info(f"Signable data for {args.name} with GUID: {args.guid}") logger.info(f"Signable data saved to: {output_file}") + logger.info(f"Receipt saved to: {receipt_file}") + logger.info(f"To attach a signature later, use: --receipt-file {receipt_file}") return 0 @@ -113,10 +280,10 @@ def _sign_with_pfx(builder: EfiVariableAuthentication2Builder, args: argparse.Na Status code (0 for success). """ # Load the signing certificate from the PFX file - with open(args.pfx_file, 'rb') as f: + with open(args.pfx_file, "rb") as f: pfx_data = f.read() - password = getpass("Enter the password for the PFX file: ").encode('utf-8') + password = getpass("Enter the password for the PFX file: ").encode("utf-8") pkcs12_store = pkcs12.load_pkcs12(pfx_data, password) # Sign the variable @@ -132,6 +299,84 @@ def _sign_with_pfx(builder: EfiVariableAuthentication2Builder, args: argparse.Na logger.info(f"Signed variable saved to: {output_file}") return 0 + +def _attach_signature_from_receipt(args: argparse.Namespace) -> int: + """Attaches a pre-generated PKCS#7 signature using metadata from a receipt file. + + This function reads a receipt file generated during signable data creation + and uses it to attach a signature with the correct metadata. + + Parameters + ---------- + args : argparse.Namespace + The parsed command-line arguments containing the receipt file path and signature file path. + + Returns: + ------- + int + Status code (0 for success). + """ + # Read the receipt file + try: + with open(args.receipt_file, "r") as f: + receipt = json.load(f) + except FileNotFoundError: + logger.error(f"Receipt file not found: {args.receipt_file}") + return 1 + except json.JSONDecodeError as e: + logger.error(f"Invalid receipt file format: {e}") + return 1 + + logger.info(f"Using receipt file: {args.receipt_file}") + logger.info(f"Using pre-generated signature from: {args.signature_file}") + + # Validate required fields in receipt + required_fields = ["variable_name", "variable_guid", "data_file", "signing_timestamp"] + for field in required_fields: + if field not in receipt: + logger.error(f"Missing required field in receipt: {field}") + return 1 + + # Read the signature file (PKCS#7 signature) + with open(args.signature_file, "rb") as f: + signature_data = f.read() + + # Read the variable payload data from the path in the receipt + try: + with open(receipt["data_file"], "rb") as f: + payload_data = f.read() + except FileNotFoundError: + logger.error(f"Variable data file not found: {receipt['data_file']}") + logger.error("The data file path in the receipt may have changed since the receipt was created.") + return 1 + + # Parse the timestamp from the receipt + try: + signing_time = datetime.datetime.fromisoformat(receipt["signing_timestamp"]) + logger.info(f"Using timestamp from receipt: {signing_time.isoformat()}") + except ValueError: + logger.error(f"Invalid timestamp in receipt: {receipt['signing_timestamp']}") + return 1 + + # Create EfiVariableAuthentication2 structure + auth_var = EfiVariableAuthentication2(time=signing_time) + auth_var.auth_info.add_cert_data(signature_data) + payload_stream = io.BytesIO(payload_data) + auth_var.set_payload(payload_stream) + + # Encode the complete authenticated variable structure + auth_var_data = auth_var.encode() + + # Save the signed variable + output_file = os.path.join(args.output_dir, f"{receipt['variable_name']}.authvar.bin") + with open(output_file, "wb") as f: + f.write(auth_var_data) + + logger.info(f"Variable with attached signature: {receipt['variable_name']} with GUID: {receipt['variable_guid']}") + logger.info(f"Signed variable saved to: {output_file}") + return 0 + + def describe_variable(args: argparse.Namespace) -> int: """Parses and describes an authenticated variable structure. @@ -147,13 +392,13 @@ def describe_variable(args: argparse.Namespace) -> int: Status code (0 for success). """ auth_var = None - with open(args.signed_payload, 'rb') as f: + with open(args.signed_payload, "rb") as f: auth_var = EfiVariableAuthentication2(decodefs=f) name = os.path.basename(args.signed_payload) output_file = os.path.join(args.output_dir, f"{name}.authvar.txt") - with open(output_file, 'w') as f: + with open(output_file, "w") as f: auth_var.print(outfs=f) logger.info(f"Output: {output_file}") @@ -169,54 +414,127 @@ def typecheck_file_exists(filepath: str) -> str: :return: valid filepath """ if not os.path.isfile(filepath): - raise argparse.ArgumentTypeError( - f"You sure this is a valid filepath? : {filepath}") + raise argparse.ArgumentTypeError(f"You sure this is a valid filepath? : {filepath}") return filepath + +def setup_format_parser(subparsers: argparse._SubParsersAction) -> argparse._SubParsersAction: + """Sets up the format parser for generating signable data and receipts. + + :param subparsers: - sub parser from argparse to add options to + + :returns: subparser + """ + format_parser = subparsers.add_parser( + "format", help="Formats variables for external signing by generating signable data and receipt files" + ) + format_parser.set_defaults(function=format_variable) + + format_parser.add_argument("name", help="UTF16 Formatted Name of Variable") + + format_parser.add_argument( + "guid", + type=uuid.UUID, + help="UUID of the namespace the variable belongs to. (Ex. 12345678-1234-1234-1234-123456789abc)", + ) + + format_parser.add_argument("attributes", help='Variable Attributes, AT is a required attribute (Ex. "NV,BS,RT,AT")') + + format_parser.add_argument( + "data_file", + help="Binary file of variable data. An empty file is accepted and will be used to clear the authenticated data", + ) + + format_parser.add_argument( + "--timestamp", + default=None, + help="Timestamp to use for the authenticated variable in ISO 8601 format (YYYY-MM-DDTHH:MM:SS). " + "If not provided, current UTC time will be used. Example: 2025-01-15T10:30:45", + ) + + format_parser.add_argument( + "--output-dir", default="./", help="Output directory for the signable data and receipt file" + ) + + return subparsers + + def setup_sign_parser(subparsers: argparse._SubParsersAction) -> argparse._SubParsersAction: - """Sets up the sign parser. + """Sets up the sign parser for signing variables or attaching pre-generated signatures. :param subparsers: - sub parser from argparse to add options to :returns: subparser """ sign_parser = subparsers.add_parser( - "sign", help="Signs variables using the command line" + "sign", help="Signs variables using PFX files or attaches pre-generated signatures" ) sign_parser.set_defaults(function=sign_variable) sign_parser.add_argument( - "name", - help="UTF16 Formated Name of Variable" + "name", nargs="?", help="UTF16 Formatted Name of Variable (not required when using --receipt-file)" ) sign_parser.add_argument( - "guid", type=uuid.UUID, + "guid", + nargs="?", + type=uuid.UUID, help="UUID of the namespace the variable belongs to. (Ex. 12345678-1234-1234-1234-123456789abc)" + " (not required when using --receipt-file)", ) sign_parser.add_argument( "attributes", + nargs="?", help="Variable Attributes, AT is a required attribute (Ex. \"NV,BS,RT,AT\")" + " (not required when using --receipt-file)" ) sign_parser.add_argument( - "data_file", type=typecheck_file_exists, - help="Binary file of variable data. An empty file is accepted and will be used to clear the authenticated data" + "data_file", + nargs="?", + help="Binary file of variable data. An empty file is accepted and will be used to clear" + "the authenticated data (not required when using --receipt-file)", + ) + + # Create mutually exclusive group for signing methods + signing_group = sign_parser.add_mutually_exclusive_group(required=True) + + signing_group.add_argument( + "--pfx-file", + default=None, + help="Pkcs12 certificate to sign the authenticated data with (Cert.pfx). " + "Use this for direct signing with a private key.", + ) + + signing_group.add_argument( + "--signature-file", + default=None, + type=typecheck_file_exists, + help="Pre-generated PKCS#7 signature file (.bin.p7) to attach to the variable. " + "Use this to attach signatures generated externally.", ) sign_parser.add_argument( - "--pfx-file", default=None, - help="Pkcs12 certificate to sign the authenticated data with (Cert.pfx)." \ - " If not provided, outputs the signable data instead." + "--receipt-file", + default=None, + type=typecheck_file_exists, + help="Receipt file (.receipt.json) generated during signable data creation. " + "When used with --signature-file, all variable metadata will be read from the receipt, " + "eliminating the need to specify name, GUID, attributes, data-file, and timestamp.", ) sign_parser.add_argument( - "--output-dir", default="./", - help="Output directory for the signed data" + "--timestamp", + default=None, + help="Timestamp to use for the authenticated variable in ISO 8601 format (YYYY-MM-DDTHH:MM:SS). " + "If not provided, current UTC time will be used. Only used when not using --receipt-file. " + "Example: 2025-01-15T10:30:45", ) + sign_parser.add_argument("--output-dir", default="./", help="Output directory for the signed data") + return subparsers @@ -227,33 +545,32 @@ def setup_describe_parser(subparsers: argparse._SubParsersAction) -> argparse._S :returns: subparser """ - describe_parser = subparsers.add_parser( - "describe", help="Parses Authenticated Variable 2 structures" - ) + describe_parser = subparsers.add_parser("describe", help="Parses Authenticated Variable 2 structures") describe_parser.set_defaults(function=describe_variable) - describe_parser.add_argument( - "signed_payload", type=typecheck_file_exists, - help="Signed payload to parse" - ) + describe_parser.add_argument("signed_payload", type=typecheck_file_exists, help="Signed payload to parse") - describe_parser.add_argument( - "--output-dir", default="./", - help="Output directory for the described data" - ) + describe_parser.add_argument("--output-dir", default="./", help="Output directory for the described data") return subparsers + def parse_args() -> argparse.Namespace: """Parses arguments from the command line.""" - parser = argparse.ArgumentParser() - subparsers = parser.add_subparsers() + parser = argparse.ArgumentParser( + description="UEFI Authenticated Variable Tool for signing and formatting variables" + ) + subparsers = parser.add_subparsers( + title="Available commands", + description="Use these commands to work with UEFI authenticated variables", + help="Command to execute", + ) parser.add_argument( - "--debug", action='store_true', default=False, - help="enables debug printing for deep inspection" + "--debug", action="store_true", default=False, help="enables debug printing for deep inspection" ) + subparsers = setup_format_parser(subparsers) subparsers = setup_sign_parser(subparsers) subparsers = setup_describe_parser(subparsers) @@ -285,4 +602,5 @@ def main() -> None: sys.exit(status_code) -main() +if __name__ == "__main__": + main() From 48330112b86f2430b965f530ca3dad1d658c4916 Mon Sep 17 00:00:00 2001 From: Doug Flick Date: Fri, 11 Jul 2025 11:20:09 -0700 Subject: [PATCH 6/6] removing unused defines --- scripts/auth_var_tool.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/scripts/auth_var_tool.py b/scripts/auth_var_tool.py index a005497..f60d075 100644 --- a/scripts/auth_var_tool.py +++ b/scripts/auth_var_tool.py @@ -54,10 +54,6 @@ # Puts the script into debug mode, may be enabled via argparse ENABLE_DEBUG = False -# Index into the certificate argument -CERTIFICATE_FILE_PATH = 0 -CERTIFICATE_PASSWORD = 1 - logging.basicConfig() logger = logging.getLogger() logger.setLevel(logging.INFO)