Skip to content

Commit 79cbb07

Browse files
committed
Example code to replace the an old PlatformKey with a new one.
Some platforms may have lost the private portion of the PlatformKey, or the PlatformKey may have been compromised. In these cases, it is necessary to replace the PlatformKey with a new one. This example demonstrates how to replace the old PlatformKey with a new one. That is intended to be used as a reference. Targets a Test certificate that is not provided to prevent accidents.
1 parent ea87358 commit 79cbb07

File tree

5 files changed

+750
-0
lines changed

5 files changed

+750
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
from edk2toollib.utility_functions import export_c_type_array
2+
3+
from edk2toollib.uefi.authenticated_variables_structure_support import (
4+
EfiSignatureDataFactory,
5+
EfiSignatureList,
6+
)
7+
8+
from tempfile import TemporaryFile
9+
from typing import Union
10+
11+
import uuid
12+
import base64
13+
import hashlib
14+
15+
possible_targets = [
16+
"Certs/PlatformKey.der"
17+
]
18+
19+
replacement_cert = ("Certs/WindowsOEMDevicesPK.der",
20+
"77fa9abd-0359-4d32-bd60-28f4e78f784b")
21+
22+
23+
def _is_pem_encoded(certificate_data: Union[str, bytes]) -> bool:
24+
"""This function is used to check if a certificate is pem encoded (base64 encoded).
25+
26+
Args:
27+
certificate_data (str | bytes): The certificate to check.
28+
29+
Returns:
30+
bool: True if the certificate is pem encoded, False otherwise.
31+
"""
32+
try:
33+
if isinstance(certificate_data, str):
34+
# If there's any unicode here, an exception will be thrown and the function will return false
35+
sb_bytes = bytes(certificate_data, "ascii")
36+
elif isinstance(certificate_data, bytes):
37+
sb_bytes = certificate_data
38+
else:
39+
raise ValueError("Argument must be string or bytes")
40+
41+
return base64.b64encode(base64.b64decode(sb_bytes)) == sb_bytes
42+
except Exception:
43+
return False
44+
45+
46+
def _convert_pem_to_der(certificate_data: Union[str, bytes]) -> bytes:
47+
"""This function is used to convert a pem encoded certificate to a der encoded certificate.
48+
49+
Args:
50+
certificate_data: The certificate to convert.
51+
52+
Returns:
53+
bytes: The der encoded certificate.
54+
"""
55+
if isinstance(certificate_data, str):
56+
# If there's any unicode here, an exception will be thrown and the function will return false
57+
certificate_data = bytes(certificate_data, "ascii")
58+
59+
return base64.b64decode(certificate_data)
60+
61+
62+
def _convert_crt_to_signature_list(file: str, signature_owner: str, **kwargs: any) -> bytes:
63+
"""This function converts a single crt file to a signature list.
64+
65+
Args:
66+
file: The path to the crt file
67+
signature_owner: The signature owner. Defaults to DEFAULT_MS_SIGNATURE_GUID.
68+
69+
Optional Args:
70+
**kwargs: Additional arguments to be passed to the function (These will be intentionally ignored)
71+
72+
Returns:
73+
bytes: The signature list
74+
"""
75+
if signature_owner is not None and not isinstance(signature_owner, uuid.UUID):
76+
signature_owner = uuid.UUID(signature_owner)
77+
78+
siglist = EfiSignatureList(
79+
typeguid=EfiSignatureDataFactory.EFI_CERT_X509_GUID)
80+
81+
with open(file, "rb") as crt_file, TemporaryFile() as temp_file:
82+
certificate = crt_file.read()
83+
if _is_pem_encoded(certificate):
84+
certificate = _convert_pem_to_der(certificate)
85+
86+
temp_file.write(certificate)
87+
temp_file.seek(0)
88+
89+
sigdata = EfiSignatureDataFactory.create(
90+
EfiSignatureDataFactory.EFI_CERT_X509_GUID, temp_file, signature_owner)
91+
92+
# X.509 certificates are variable size, so they must be contained in their own signature list
93+
siglist.AddSignatureHeader(None, SigSize=sigdata.get_total_size())
94+
siglist.AddSignatureData(sigdata)
95+
96+
return siglist.encode()
97+
98+
99+
def generate_header_file(possible_targets, replacement_cert):
100+
"""
101+
Generates a C header file named 'TargetList.h' that contains potential target
102+
SHA256 hashes and a new platform key signature list.
103+
The function performs the following steps:
104+
1. Iterates over a list of possible targets, converting each target's certificate
105+
to an EFI_SIGNATURE_LIST and appending it to a list.
106+
2. Converts a replacement certificate to an EFI_SIGNATURE_LIST.
107+
3. Opens (or creates) 'TargetList.h' for writing.
108+
4. Writes the necessary header guards and includes to the file.
109+
5. Iterates over the possible targets again, converting each to a SHA256 hash,
110+
and writes these hashes to the header file in a specific format.
111+
6. Writes the new platform key signature list to a temporary file and exports it
112+
as a C type array to the header file.
113+
7. Closes the header file.
114+
"""
115+
116+
117+
# Convert the cert to an EFI_SIGNATURE_LIST
118+
new_platform_key_sig_list = _convert_crt_to_signature_list(
119+
file=replacement_cert[0], signature_owner=replacement_cert[1])
120+
121+
with open("PlatformKey.h", 'w') as header_file:
122+
header_file.write("#ifndef PLATFORM_KEY_H_\n")
123+
header_file.write("#define PLATFORM_KEY_H_\n\n")
124+
125+
header_file.write("#include <Uefi.h>\n")
126+
header_file.write("#include <Library/BaseCryptLib.h>\n")
127+
128+
targets = []
129+
130+
for target in possible_targets:
131+
# Convert the cert to a signature list
132+
with open(target, 'rb') as f:
133+
134+
# Calculate the SHA256 hash of the signature list
135+
sha256_hash = hashlib.sha256()
136+
sha256_hash.update(f.read())
137+
targets.append((sha256_hash.hexdigest(), target))
138+
139+
header_file.write("\n//\n")
140+
header_file.write("//Number of Hashes to check against\n")
141+
header_file.write("//\n")
142+
header_file.write(f"#define POTENTIAL_TARGETS {len(targets)}\n\n")
143+
144+
header_file.write("\n//\n")
145+
header_file.write(
146+
"// List of certificate hashes that should be targeted\n")
147+
header_file.write("//\n")
148+
header_file.write(
149+
f"UINT8 PotentialTargetsHashes[POTENTIAL_TARGETS][SHA256_DIGEST_SIZE] = {{\n")
150+
for target in targets:
151+
byte_array = bytes.fromhex(target[0])
152+
first_half = ', '.join(f'0x{byte_array[i]:02x}' for i in range(16))
153+
first_half += ','
154+
second_half = ', '.join(
155+
f'0x{byte_array[i]:02x}' for i in range(16, 32))
156+
157+
header_file.write(
158+
f" {{ // SHA256: {target[0]} ({target[1]})\n")
159+
header_file.write(f" {first_half}\n")
160+
header_file.write(f" {second_half}\n")
161+
header_file.write(" },\n")
162+
header_file.write("};\n\n")
163+
164+
header_file.write("//\n")
165+
header_file.write(
166+
"// In production this should be PkDefault via either SecureBootFetchData(..) or FixedPcdGetPtr (PcdDefaultPk)\n")
167+
header_file.write(f"// Certificate: {replacement_cert[0]}\n")
168+
header_file.write("//\n")
169+
170+
with TemporaryFile() as new_platform_key:
171+
new_platform_key.write(new_platform_key_sig_list)
172+
new_platform_key.seek(0)
173+
174+
export_c_type_array(
175+
new_platform_key, "NewPlatformKey", header_file)
176+
177+
header_file.write("#endif // PLATFORM_KEY_H_\n")
178+
179+
180+
generate_header_file(possible_targets, replacement_cert)
+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
2+
3+
4+
$LASTEXITCODE = 0
5+
function GenerateCertificate {
6+
<#
7+
This function generates a certificate used for mock testing
8+
9+
:param KeyLength: The size in bits of the length of the key (ex 2048)
10+
:param CommonName: Common name field of the certificate
11+
:param Variable Name: Name of the variable (Not important for the certificate but used to track which pfx is tied to which signed data)
12+
:param VariablePrefix: Prefix to append to the beginning of the certificate for tracking (Not Important)
13+
:param Signer: Signing certificate object from the Certificate Store
14+
15+
:return: HashTable Object
16+
{
17+
.Cert # Path to Certificate in the Certificate Store
18+
.CertPath # Path to the der encoded certificate
19+
.PfxCertPath # Path to the pfx file generated
20+
}
21+
#>
22+
23+
# Get-ChildItem -Path "Cert:\LocalMachine\My" | Where-Object Thumbprint -EQ 4EFF6B1A0F61B4BF692C77F09889AD151EE8BB58 | Select-Object *
24+
25+
param (
26+
$CertificateParams,
27+
$VariableName,
28+
$VariablePrefix
29+
)
30+
31+
# Return object on success
32+
$PfxCertFilePath = Join-Path -Path $Globals.Layout.CertificateFolder -ChildPath "${VariablePrefix}${VariableName}.pfx"
33+
$CertFilePath = Join-Path -Path $Globals.Layout.CertificateFolder -ChildPath "${VariablePrefix}${VariableName}.cer"
34+
$P7bCertFilePath = Join-Path -Path $Globals.Layout.CertificateFolder -ChildPath "${VariablePrefix}${VariableName}.p7b"
35+
36+
Write-Host "$> New-SelfSignedCertificate " @CertificateParams
37+
38+
# Generate the new certifcate with the chosen params
39+
$Output = New-SelfSignedCertificate @CertificateParams
40+
if ($LASTEXITCODE -ne 0) {
41+
Write-Host "New-SelfSignedCertificate Failed"
42+
Write-Host "Error Code: $LASTEXITCODE"
43+
Write-Host "Output: $Output"
44+
return $null
45+
}
46+
47+
# The path of the certificate in the store
48+
$MockCert = $Globals.Certificate.Store + $Output.Thumbprint
49+
50+
# Print all the details from the certificate
51+
# Get-ChildItem -Path $Globals.Certificate.Store | Where-Object Thumbprint -EQ $Output.Thumbprint | Select-Object * | Write-Host
52+
53+
# export the certificate as a PFX
54+
Export-PfxCertificate -Cert $MockCert -FilePath $PfxCertFilePath -Password $Globals.Certificate.SecurePassword | Out-Null
55+
if ($LASTEXITCODE -ne 0) {
56+
Write-Host "Export-PfxCertificate Failed"
57+
return $null
58+
}
59+
60+
Export-Certificate -Cert $MockCert -FilePath $CertFilePath | Out-Null
61+
if ($LASTEXITCODE -ne 0) {
62+
Write-Host "Export-Certificate Failed"
63+
return $null
64+
}
65+
66+
$ReturnObject = @{
67+
Cert = $MockCert
68+
CertPath = $CertFilePath
69+
PfxCertPath = $PfxCertFilePath
70+
CertInfo = "${PfxCertFilePath};password" # path to the pfx certificate and the password
71+
}
72+
73+
return $ReturnObject
74+
}
75+
76+
$Organization = "Project MU"
77+
$Unit = "TEST"
78+
$CommonName = "DO NOT TRUST - DO NOT USE - WHY DO YOU EVEN HAVE THIS"
79+
$KeyLength = 2048
80+
81+
$Password = "password"
82+
$DataFolder = "./Test"
83+
$TestDataName = "TestData"
84+
$CertName = "Certs"
85+
86+
# Global Variables used throughout the script
87+
$Globals = @{
88+
Certificate = @{
89+
Store = "Cert:\LocalMachine\My\"
90+
Organization = "ProjectMu"
91+
Password = $Password
92+
SecurePassword = ConvertTo-SecureString $Password -Force -AsPlainText
93+
LifeYears = 10 # How long in the future should the Certificate be valid
94+
}
95+
Layout = @{
96+
DataFolder = $DataFolder
97+
CertName = $CertName
98+
CertificateFolder = "$DataFolder/$CertName"
99+
TestDataName = $TestDataName
100+
TestDataFolder = "$DataFolder/$TestDataName"
101+
}
102+
}
103+
104+
# Create the Certificate Folder
105+
if (-not (Test-Path -Path $Globals.Layout.CertificateFolder)) {
106+
New-Item -Path $Globals.Layout.CertificateFolder -ItemType Directory | Out-Null
107+
}
108+
109+
$CertificateParams = @{
110+
DnsName = "www.$Organization.com"
111+
CertStoreLocation = $Globals.Certificate.Store
112+
KeyAlgorithm = "RSA"
113+
KeyLength = $KeyLength
114+
Subject = "CN=$CommonName O=$Organization OU=$Unit"
115+
NotAfter = (Get-Date).AddYears($Globals.Certificate.LifeYears)
116+
KeyUsage = @("CertSign", "CRLSign", "DigitalSignature")
117+
# Basic Constraint :
118+
# CA: A CA certificate, by definition, must have a basic constraints extension with this `CA` boolean value set to "true" in order to be a CA.
119+
# pathlength: Limits the number of intermediate certificates allowed by the next certificates
120+
TextExtension = @("2.5.29.19={text}CA=1")
121+
}
122+
123+
GenerateCertificate -CertificateParams $CertificateParams -VariableName "TestCert" -VariablePrefix "Test"

0 commit comments

Comments
 (0)