Skip to content

Instantly share code, notes, and snippets.

@al3xtjames
Last active January 23, 2022 20:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save al3xtjames/d61529c1fa30268a4a6c3024de61ea57 to your computer and use it in GitHub Desktop.
Save al3xtjames/d61529c1fa30268a4a6c3024de61ea57 to your computer and use it in GitHub Desktop.
Miscellaneous Secure Boot scripts
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
import datetime
import pathlib
import shutil
import ssl
import struct
import subprocess
import sys
import typing
import uuid
EFI_CERT_TYPE_PKCS7_GUID = uuid.UUID("4aafd29d-68df-49ee-8aa9-347d375665a7")
EFI_CERT_X509_GUID = uuid.UUID("a5c059a1-94e4-4aa7-87b5-ab155c2bf072")
EFI_GLOBAL_VARIABLE_GUID = uuid.UUID("8be4df61-93ca-11d2-aa0d-00e098032b8c")
EFI_IMAGE_SECURITY_DATABASE_GUID = uuid.UUID("d719b2cb-3d3a-4596-a3bc-dad00e67656f")
EFI_VARIABLE_NON_VOLATILE = 1
EFI_VARIABLE_BOOTSERVICE_ACCESS = 2
EFI_VARIABLE_RUNTIME_ACCESS = 4
EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS = 32
WIN_CERT_TYPE_EFI_GUID = 0x0EF1
WIN_CERT_REVISION = 0x0200
# UEFI Specification, version 2.8, §32.4.1: Signature Database
def gen_efi_sig_list(cert_file: pathlib.Path, owner_guid: uuid.UUID) -> bytes:
with open(cert_file, "rb") as fp:
cert = fp.read()
if cert.startswith(b"-----BEGIN CERTIFICATE-----"):
cert = ssl.PEM_cert_to_DER_cert(cert.decode("ascii"))
efi_sig_list = bytearray()
# EFI_SIGNATURE_LIST
# SignatureType
efi_sig_list.extend(EFI_CERT_X509_GUID.bytes_le)
# SignatureListSize, SignatureHeaderSize, SignatureSize
efi_sig_list.extend(struct.pack("<3I", 16 + 4 + 4 + 4 + 16 + len(cert), 0, 16 + len(cert)))
# EFI_SIGNATURE_DATA
# SignatureOwner
efi_sig_list.extend(owner_guid.bytes_le)
# SignatureData
efi_sig_list.extend(cert)
return efi_sig_list
def run(args, *, input=None, check=False, encoding=None) -> subprocess.CompletedProcess:
try:
return subprocess.run(args, input=input, capture_output=True, check=check, encoding=encoding)
except subprocess.CalledProcessError as e:
print(e.stderr.decode(), file=sys.stderr)
raise e
def openssl_gen_pkcs7_sig(data: bytes, key: str, cert_file: pathlib.Path, engine: typing.Union[str, None]) -> bytes:
openssl_args = [openssl_path, "smime", "-sign", "-binary", "-noattr", "-md", "sha256", "-outform", "der",
"-inkey", key, "-signer", cert_file]
if engine is not None:
openssl_args += ["-engine", engine, "-keyform", "engine"]
proc = run(openssl_args, check=True, input=data)
return proc.stdout
# UEFI Specification version 2.8, §8.2.2: Using the EFI_VARIABLE_AUTHENTICATION_2 descriptor
def sign_efi_sig_list(variable_name: str, efi_sig_list: bytes, auth_var_file: pathlib.Path, key: str,
cert_file: pathlib.Path, engine: typing.Union[str, None]):
timestamp = datetime.datetime.utcnow()
payload = bytearray()
# VariableName
payload.extend(variable_name.encode("utf_16_le"))
# VendorGuid
if variable_name in ("PK", "KEK"):
payload.extend(EFI_GLOBAL_VARIABLE_GUID.bytes_le)
elif variable_name in ("db", "dbx"):
payload.extend(EFI_IMAGE_SECURITY_DATABASE_GUID.bytes_le)
# Attributes
payload.extend(struct.pack("<I", EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS |
EFI_VARIABLE_RUNTIME_ACCESS | EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS))
# TimeStamp
payload.extend(struct.pack("<H6BIh2B", timestamp.year, timestamp.month, timestamp.day, timestamp.hour,
timestamp.minute, timestamp.second, 0, 0, 0, 0, 0))
# VariableContent
payload.extend(efi_sig_list)
sig = openssl_gen_pkcs7_sig(payload, key, cert_file, engine)
efi_var_auth2 = bytearray()
# EFI_VARIABLE_AUTHENTICATION_2
# TimeStamp
efi_var_auth2.extend(struct.pack("<H6BIh2B", timestamp.year, timestamp.month, timestamp.day, timestamp.hour,
timestamp.minute, timestamp.second, 0, 0, 0, 0, 0))
# dwLength, wRevision, wCertificateType
efi_var_auth2.extend(struct.pack("<I2H", 4 + 2 + 2 + 16 + len(sig), WIN_CERT_REVISION, WIN_CERT_TYPE_EFI_GUID))
# CertType
efi_var_auth2.extend(EFI_CERT_TYPE_PKCS7_GUID.bytes_le)
# CertData
efi_var_auth2.extend(sig)
# VariableContent
efi_var_auth2.extend(efi_sig_list)
with open(auth_var_file, "wb") as fp:
fp.write(efi_var_auth2)
def gen_auth_vars(pk: str, pk_cert_file: pathlib.Path, kek: str, kek_cert_file: pathlib.Path,
db_cert_files: list[pathlib.Path], dbx_file: typing.Union[str, None], owner_guid: uuid.UUID,
output_dir: pathlib.Path, engine: typing.Union[str, None]) -> None:
pk_esl = gen_efi_sig_list(pk_cert_file, owner_guid)
kek_esl = gen_efi_sig_list(kek_cert_file, owner_guid)
db_esl = bytearray()
for db_cert_file in db_cert_files:
db_esl.extend(gen_efi_sig_list(db_cert_file, owner_guid))
print("Generating PK.auth")
sign_efi_sig_list("PK", pk_esl, output_dir / "PK.auth", pk, pk_cert_file, engine)
print("Generating KEK.auth")
sign_efi_sig_list("KEK", kek_esl, output_dir / "KEK.auth", pk, pk_cert_file, engine)
print("Generating db.auth")
sign_efi_sig_list("db", db_esl, output_dir / "db.auth", kek, kek_cert_file, engine)
if dbx_file is not None:
print("Generating dbx.auth")
with open(dbx_file, "rb") as fp:
dbx = fp.read()
# The DBX files from uefi.org are already signed with Microsoft's KEK.
# Skip over EFI_VARIABLE_AUTHENTICATION_2 to get the EFI_SIGNATURE_LIST.
dbx_esl_start = 16 + struct.unpack_from("<I", dbx, 16)[0]
dbx_esl = dbx[dbx_esl_start:]
sign_efi_sig_list("dbx", dbx_esl, output_dir / "dbx.auth", kek, kek_cert_file, engine)
def check_file(file: pathlib.Path) -> None:
if not file.exists():
print(str(file), "does not exist", file=sys.stderr)
sys.exit(1)
def find_executable(name: str) -> str:
path = shutil.which(name)
if not path:
print(name, "was not found in PATH", file=sys.stderr)
sys.exit(1)
return path
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="UEFI authenticated variable generator")
parser.add_argument("-e", "--engine", type=str, help="OpenSSL engine for key operations")
parser.add_argument("-g", "--owner-guid", type=uuid.UUID, required=True, help="Owner GUID")
parser.add_argument("--pk", type=str, required=True, help="Path to platform key (or key URI for engines)")
parser.add_argument("--pk-cert", type=pathlib.Path, required=True, help="Path to platform key certificate")
parser.add_argument("--kek", type=str, required=True, help="Path to key exchange key (or key URI for engines)")
parser.add_argument("--kek-cert", type=pathlib.Path, required=True, help="Path to key exchange key certificate")
parser.add_argument("--dbx", type=pathlib.Path, help="Path to signed revocation list file (dbxupdate_x64.bin)")
parser.add_argument("-o", "--output-dir", type=pathlib.Path, help="Output directory")
parser.add_argument("db-cert", type=pathlib.Path, nargs="+",
help="Additional certificates to add to the signature database (db)")
args = parser.parse_args()
check_file(args.pk_cert)
check_file(args.kek_cert)
check_file(args.output_dir)
if args.dbx is not None:
check_file(args.dbx)
db_certs = getattr(args, "db-cert")
for cert in db_certs:
check_file(cert)
openssl_path = find_executable("openssl")
gen_auth_vars(args.pk, args.pk_cert, args.kek, args.kek_cert, db_certs, args.dbx, args.owner_guid, args.output_dir,
args.engine)
#!/usr/bin/env python3
import argparse
import hashlib
import pathlib
import plistlib
import shutil
import subprocess
import sys
import tempfile
import typing
def run(args, *, input=None, check=False, encoding=None) -> subprocess.CompletedProcess:
try:
return subprocess.run(args, input=input, capture_output=True, check=check, encoding=encoding)
except subprocess.CalledProcessError as e:
print(e.stderr.decode(), file=sys.stderr)
raise e
def verify_efi_binary(efi_file: pathlib.Path, cert_file: pathlib.Path) -> bool:
proc = run([sbverify_path, "--cert", str(cert_file), str(efi_file)])
return proc.returncode == 0
def sign_efi_binary(efi_file: pathlib.Path, key: str, cert_file: pathlib.Path,
engine: typing.Union[str, None]) -> None:
if verify_efi_binary(efi_file, cert_file):
return
proc = run([sbverify_path, "--list", str(efi_file)], check=True)
num_signatures = 0
for line in proc.stdout.splitlines():
if line.startswith(b"signature "):
num_signatures += 1
while num_signatures > 0:
run([sbattach_path, "--remove", str(efi_file)], check=True)
num_signatures -= 1
sbsign_args = [sbsign_path, "--key", key, "--cert", str(cert_file), "--output", str(efi_file)]
if engine is not None:
sbsign_args += ["--engine", engine]
sbsign_args += [str(efi_file)]
run(sbsign_args, check=True)
def openssl_verify_signed_digest(file: pathlib.Path, sig_file: pathlib.Path, cert_file: pathlib.Path) -> bool:
with tempfile.NamedTemporaryFile() as fp:
run([openssl_path, "x509", "-pubkey", "-out", fp.name, "-in", str(cert_file)], check=True)
proc = run([openssl_path, "dgst", "-sha256", "-verify", fp.name, "-signature", str(sig_file), str(file)])
return proc.returncode == 0
def openssl_gen_signed_digest(file: pathlib.Path, key: str, cert_file: pathlib.Path, sig_file: pathlib.Path,
engine: typing.Union[str, None]) -> None:
if sig_file.exists():
if openssl_verify_signed_digest(file, sig_file, cert_file):
return
else:
sig_file.unlink()
openssl_args = [openssl_path, "dgst", "-sha256", "-sign", key, "-out", str(sig_file)]
if engine is not None:
openssl_args += ["-engine", engine, "-keyform", "engine"]
openssl_args += [str(file)]
run(openssl_args, check=True)
def gen_oc_rsa_pub_key(cert_file: pathlib.Path) -> bytes:
proc = run([openssl_path, "x509", "-modulus", "-noout", "-in", str(cert_file)])
modulus_bytes = bytearray.fromhex(proc.stdout.split(b"Modulus=")[1].decode())
key_bits = len(modulus_bytes) * 8
if key_bits != 2048:
raise Exception("Invalid modulus length")
key_qwords = len(modulus_bytes) // 8
N = int.from_bytes(modulus_bytes, byteorder="big")
B = 2 ** 64
N0_inv = B - pow(N, -1, B)
if N0_inv > 0xFFFFFFFFFFFFFFFF:
raise Exception("Invalid N0 inverse")
R = 2 ** key_bits
R_2_mod_N = pow(R, 2, N)
rsa_pub_key = bytearray()
# OC_RSA_PUBLIC_KEY_2048
# NumQwords
rsa_pub_key.extend(key_qwords.to_bytes(2, byteorder="little"))
# Reserved
rsa_pub_key.extend(int(0).to_bytes(6, byteorder="little"))
# N0Inv
rsa_pub_key.extend(N0_inv.to_bytes(8, byteorder="little"))
# Modulus
rsa_pub_key.extend(N.to_bytes(len(modulus_bytes), byteorder="little"))
# RSqrMod
rsa_pub_key.extend(R_2_mod_N.to_bytes(len(modulus_bytes), byteorder="little"))
return rsa_pub_key
def sign_oc(oc_path: pathlib.Path, key: str, cert_file: pathlib.Path, engine: typing.Union[str, None]) -> None:
# Sign all EFI binaries in EFI/OC (excluding OpenCore.efi, as this may be binpatched during vaulting).
oc_files = sorted(filter(lambda file: file.is_file() and file.name[0] != "." and file.name != "OpenCore.efi" and
file.stem != "vault", oc_path.rglob("*")))
efi_files = filter(lambda file: file.suffix == ".efi" and file.name != "OpenCore.efi", oc_files)
for file in efi_files:
print("Signing", str(file.relative_to(oc_path)))
sign_efi_binary(file, key, cert_file, engine)
# Generate vault.plist from all (unhidden) files in EFI/OC.
print("Vaulting OC")
vault = {"Files": {}, "Version": 1}
for file in oc_files:
with open(file, "rb") as fp:
data = fp.read()
h = hashlib.sha256()
h.update(data)
file_path = str(pathlib.PureWindowsPath(file.relative_to(oc_path)))
vault["Files"][file_path] = h.digest()
vault_path = oc_path / "vault.plist"
with open(vault_path, "wb") as fp:
plistlib.dump(vault, fp)
# Sign vault.plist.
vault_sig_path = oc_path / "vault.sig"
openssl_gen_signed_digest(vault_path, key, cert_file, vault_sig_path, engine)
# Binpatch OpenCore.efi to embed the public key used for vault.sig.
pub_key = gen_oc_rsa_pub_key(cert_file)
oc_binary_path = oc_path / "OpenCore.efi"
with open(oc_binary_path, "rb") as fp:
oc_binary = fp.read()
oc_binary_pub_key_start = oc_binary.find(b"=BEGIN OC VAULT=") + len(b"=BEGIN OC VAULT=")
oc_binary_pub_key_end = oc_binary.find(b"==END OC VAULT==")
if oc_binary_pub_key_end - oc_binary_pub_key_start != len(pub_key):
print("Invalid vault magic in OpenCore.efi", file=sys.stder)
sys.exit(1)
oc_binary = oc_binary[:oc_binary_pub_key_start] + pub_key + oc_binary[oc_binary_pub_key_end:]
with open(oc_binary_path, "wb") as fp:
fp.write(oc_binary)
# Finally sign OpenCore.efi.
print("Signing OpenCore.efi")
sign_efi_binary(oc_binary_path, key, cert_file, engine)
def check_file(file: pathlib.Path) -> None:
if not file.exists():
print(str(file), "does not exist", file=sys.stderr)
sys.exit(1)
def find_executable(name: str) -> str:
path = shutil.which(name)
if not path:
print(name, "was not found in PATH", file=sys.stderr)
sys.exit(1)
return path
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="OpenCore UEFI Secure Boot/vault signing utility")
parser.add_argument("-e", "--engine", type=str, help="OpenSSL engine for key operations")
parser.add_argument("-k", "--key", type=str, required=True, help="Path to signing key (or key URI for engines)")
parser.add_argument("-c", "--cert", type=pathlib.Path, required=True, help="Path to certificate")
parser.add_argument("oc-path", type=pathlib.Path, help="Path to OpenCore (EFI/OC)")
args = parser.parse_args()
check_file(args.cert)
oc_path = getattr(args, "oc-path")
if not oc_path.exists() or not (oc_path / "OpenCore.efi").exists():
print("OpenCore.efi was not found in", str(oc_path), file=sys.stderr)
sys.exit(1)
openssl_path = find_executable("openssl")
sbattach_path = find_executable("sbattach")
sbsign_path = find_executable("sbsign")
sbverify_path = find_executable("sbverify")
sign_oc(oc_path, args.key, args.cert, args.engine)
# References:
# https://habr.com/ru/post/273497/
# https://wiki.gentoo.org/wiki/User:Sakaki/Sakaki%27s_EFI_Install_Guide/Configuring_Secure_Boot_under_OpenRC
# https://jade.fyi/blog/tpm-ssh/
# https://incenp.org/notes/2020/tpm-based-ssh-key.html
# https://www.evolware.org/?p=597
# Silence FAPI warnings
export TSS2_LOG=fapi+NONE
# Initialize store
tpm2_ptool init
# Generate Secure Boot keys (PK, KEK, ISK)
tpm2_ptool addtoken --pid 1 --label PK --sopin $PK_PIN --userpin $PK_PIN
tpm2_ptool addtoken --pid 1 --label KEK --sopin $KEK_PIN --userpin $KEK_PIN
tpm2_ptool addtoken --pid 1 --label ISK --sopin $ISK_PIN --userpin $ISK_PIN
tpm2_ptool addkey --algorithm rsa2048 --label PK --userpin $PK_PIN
tpm2_ptool addkey --algorithm rsa2048 --label KEK --userpin $KEK_PIN
tpm2_ptool addkey --algorithm rsa2048 --label ISK --userpin $ISK_PIN
# Obtain public key certificates
openssl req -new -x509 -newkey rsa:2048 -sha256 -days 1825 \
-subj "/CN=Platform Key" -engine pkcs11 -keyform engine \
-key "pkcs11:token=PK;pin-value=$PK_PIN" -out PK.pem
openssl req -new -x509 -newkey rsa:2048 -sha256 -days 1825 \
-subj "/CN=Key Exchange Key" -engine pkcs11 -keyform engine \
-key "pkcs11:token=KEK;pin-value=$KEK_PIN" -out KEK.pem
openssl req -new -x509 -newkey rsa:2048 -sha256 -days 1825 \
-subj "/CN=Image Signing Key" -engine pkcs11 -keyform engine \
-key "pkcs11:token=ISK;pin-value=$ISK_PIN" -out ISK.pem
# Download MS UEFI certs (DB) and revocation list (DBX)
# Add other certs if needed (e.g. FedoraSecureBootCA.pem)
curl -O 'https://uefi.org/sites/default/files/resources/dbxupdate_x64.bin'
curl -L 'https://go.microsoft.com/fwlink/p/?linkid=321192' \
-o MicWinProPCA2011_2011-10-19.crt
curl -L 'https://go.microsoft.com/fwlink/p/?linkid=321194' \
-o MicCorUEFCA2011_2011-06-27.crt
# Generate authenticated variables
# Copy .auth files to to ESP or some other volume accessible in setup interface
uuidgen > owner_guid.txt
python3 authvargen.py -e pkcs11 -g $(< owner_guid.txt) \
--pk "pkcs11:token=PK;pin-value=$PK_PIN" --pk-cert PK.pem \
--kek "pkcs11:token=KEK;pin-value=$KEK_PIN" --kek-cert KEK.pem \
--dbx dbxupdate_x64.bin -o . ISK.pem FedoraSecureBootCA.pem \
MicWinProPCA2011_2011-10-19.crt MicCorUEFCA2011_2011-06-27.crt
# Sign and vault OpenCore
python3 ocsign.py -e pkcs11 -k "pkcs11:token=ISK;pin-value=$ISK_PIN" \
-c ISK.pem /path/to/EFI/OC
# Bonus: Use DKMS sign helper to sign Linux kernel modules
# Link store in root's home and copy ISK public key certificate (as DER)
sudo ln -s "$HOME/.tpm2_pkcs11" /root
sudo openssl x509 -in ISK.pem -out /root/ISK.der -outform DER
# Update DKMS sign helper to use ISK
sudo mv /etc/dkms/sign_helper.sh /etc/dkms/sign_helper.sh.bak
sudo tee /etc/dkms/sign_helper.sh <<'EOF'
#!/usr/bin/sh
/lib/modules/"$1"/build/scripts/sign-file sha512 "pkcs11:token=ISK" /root/ISK.der "$2"
'EOF'
# TODO: reinstall DKMS modules
# Reboot into setup interface, enable secure boot, and enroll authenticated
# variables
# References:
# https://habr.com/ru/post/273497/
# https://wiki.gentoo.org/wiki/User:Sakaki/Sakaki%27s_EFI_Install_Guide/Configuring_Secure_Boot_under_OpenRC
# Generate Secure Boot keys (PK, KEK, ISK)
openssl req -new -x509 -newkey rsa:2048 -sha256 -days 1825 \
-subj "/CN=Platform Key" -keyout PK.key -out PK.pem
openssl req -new -x509 -newkey rsa:2048 -sha256 -days 1825 \
-subj "/CN=Key Exchange Key" -keyout KEK.key -out KEK.pem
openssl req -new -x509 -newkey rsa:2048 -sha256 -days 1825 \
-subj "/CN=Image Signing Key" -keyout ISK.key -out ISK.pem
# Download MS UEFI certs (DB) and revocation list (DBX)
# Add other certs if needed (e.g. FedoraSecureBootCA.pem)
curl -O 'https://uefi.org/sites/default/files/resources/dbxupdate_x64.bin'
curl -L 'https://go.microsoft.com/fwlink/p/?linkid=321192' \
-o MicWinProPCA2011_2011-10-19.crt
curl -L 'https://go.microsoft.com/fwlink/p/?linkid=321194' \
-o MicCorUEFCA2011_2011-06-27.crt
# Generate authenticated variables
# Copy .auth files to to ESP or some other volume accessible in setup interface
uuidgen > owner_guid.txt
python3 authvargen.py -g $(< owner_guid.txt) --pk PK.key --pk-cert PK.pem \
--kek KEK.key --kek-cert KEK.pem --dbx dbxupdate_x64.bin -o . ISK.pem \
FedoraSecureBootCA.pem MicWinProPCA2011_2011-10-19.crt \
MicCorUEFCA2011_2011-06-27.crt
# Sign and vault OpenCore
python3 ocsign.py -k ISK.key -c ISK.pem /path/to/EFI/OC
# Reboot into setup interface, enable secure boot, and enroll authenticated
# variables
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment