From 7ff26e3634d3f80dbc78a5cdd0606afe97cc2022 Mon Sep 17 00:00:00 2001 From: "dougflick@microsoft.com" Date: Wed, 22 Apr 2026 13:24:25 -0700 Subject: [PATCH] Build: Detect and strip PKCS#7 ContentInfo wrappers in KEK updates Some KEK update files are generated with an outer ContentInfo SEQUENCE wrapping the SignedData blob. Older EDK2-based firmware expects raw SignedData directly in WIN_CERTIFICATE_UEFI_GUID.CertData; ContentInfo support was only added recently in: tianocore/edk2@37d3eb026a766b2405daae47e02094c2ec248646 This is normally because the tool that used wanted to sign it in a "normal" way where the code in UEFI is anything but normal. If we strip off the ContentInfo(..) we have a greater chance of supporting platforms stuck on older UEFI firmware. Signed-of-by: Doug Flick --- .github/workflows/validate-kek-updates.yml | 13 ++- scripts/strip_content_info.py | 92 ++++++++++++++++++++++ scripts/validate_kek.py | 42 ++++++++++ 3 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 scripts/strip_content_info.py diff --git a/.github/workflows/validate-kek-updates.yml b/.github/workflows/validate-kek-updates.yml index 397eac0..6ab3800 100644 --- a/.github/workflows/validate-kek-updates.yml +++ b/.github/workflows/validate-kek-updates.yml @@ -94,10 +94,11 @@ jobs: # Parse JSON to check both signature and payload SIGNATURE_VALID=$(jq -r '.result.valid' "$OUTPUT_JSON") PAYLOAD_VALID=$(jq -r '.result.payload_hash_valid' "$OUTPUT_JSON") + CONTENT_INFO_WRAPPED=$(jq -r '.result.content_info_wrapped' "$OUTPUT_JSON") JSON_CONTENT=$(cat "$OUTPUT_JSON") ALL_JSON="${ALL_JSON}### ${file}\n\`\`\`json\n${JSON_CONTENT}\n\`\`\`\n\n" - if [ "$SIGNATURE_VALID" = "true" ] && [ "$PAYLOAD_VALID" = "true" ]; then + if [ "$SIGNATURE_VALID" = "true" ] && [ "$PAYLOAD_VALID" = "true" ] && [ "$CONTENT_INFO_WRAPPED" = "false" ]; then echo "✅ **PASS**: \`$file\`" >> $GITHUB_STEP_SUMMARY echo " - Cryptographic Signature: ✅ VALID" >> $GITHUB_STEP_SUMMARY echo " - Expected Payload: ✅ True" >> $GITHUB_STEP_SUMMARY @@ -108,6 +109,16 @@ jobs: PAYLOAD_HASH=$(jq -r '.result.payload_hash' "$OUTPUT_JSON") echo " - Payload Hash: \`$PAYLOAD_HASH\`" >> $GITHUB_STEP_SUMMARY # Don't fail on payload mismatch, just warn + elif [ "$CONTENT_INFO_WRAPPED" = "true" ]; then + echo "⚠️ **WARNING**: \`$file\`" >> $GITHUB_STEP_SUMMARY + echo " - Cryptographic Signature: ✅ VALID" >> $GITHUB_STEP_SUMMARY + echo " - Expected Payload: ✅ True" >> $GITHUB_STEP_SUMMARY + echo " - ContentInfo Wrapper: ⚠️ Detected" >> $GITHUB_STEP_SUMMARY + echo " > **Why this matters:** The PKCS\#7 signature contains an outer \`ContentInfo\` SEQUENCE" >> $GITHUB_STEP_SUMMARY + echo " > wrapping the \`SignedData\`. Older EDK2-based firmware expects raw \`SignedData\` in" >> $GITHUB_STEP_SUMMARY + echo " > \`WIN_CERTIFICATE_UEFI_GUID.CertData\` and will reject the update. Use" >> $GITHUB_STEP_SUMMARY + echo " > \`scripts/strip_content_info.py\` to remove the wrapper before submitting." >> $GITHUB_STEP_SUMMARY + # Don't fail on ContentInfo wrapper, just warn else echo "❌ **FAIL**: \`$file\`" >> $GITHUB_STEP_SUMMARY echo " - Cryptographic Signature: ❌ INVALID" >> $GITHUB_STEP_SUMMARY diff --git a/scripts/strip_content_info.py b/scripts/strip_content_info.py new file mode 100644 index 0000000..1f8b2b9 --- /dev/null +++ b/scripts/strip_content_info.py @@ -0,0 +1,92 @@ +# @file +# +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: BSD-2-Clause-Patent +## +"""Strip PKCS#7 ContentInfo wrappers from EFI auth variable signatures. + +Some tooling expects the certificate payload to be raw SignedData instead of a +ContentInfo wrapper. This script rewrites an authenticated variable payload by +replacing cert_data with DER-encoded SignedData. +""" + +import argparse +import logging +import pathlib +import sys + +from edk2toollib.uefi.authenticated_variables_structure_support import EfiVariableAuthentication2 +from pyasn1.codec.der.decoder import decode as der_decode +from pyasn1.codec.der.encoder import encode as der_encode +from pyasn1_modules import rfc2315 + + +def pkcs7_get_signed_data_structure(signature: bytes) -> bytes: + """Return DER-encoded SignedData from a DER PKCS#7 payload. + + The input may be either ContentInfo(signedData) or SignedData directly. + """ + try: + content_info, _ = der_decode(signature, asn1Spec=rfc2315.ContentInfo()) + content_type = content_info.getComponentByName("contentType") + if content_type != rfc2315.signedData: + raise ValueError("PKCS#7 payload is not signedData content") + + signed_data, _ = der_decode( + content_info.getComponentByName("content"), + asn1Spec=rfc2315.SignedData(), + ) + logging.info("Found PKCS#7 ContentInfo(signedData); stripping ContentInfo wrapper") + return der_encode(signed_data) + except Exception as content_info_error: + logging.debug("ContentInfo decode failed: %s", content_info_error) + logging.info("Input does not decode as ContentInfo; trying SignedData") + + try: + signed_data, _ = der_decode(signature, asn1Spec=rfc2315.SignedData()) + logging.info("Input already decodes as SignedData") + return der_encode(signed_data) + except Exception as signed_data_error: + raise ValueError( + "Signature is neither ContentInfo(signedData) nor SignedData" + ) from signed_data_error + + +def strip_content_info(signed_payload: pathlib.Path) -> pathlib.Path: + """Rewrite signed_payload with cert_data set to raw SignedData. + + Returns the path of the rewritten output file. + """ + with open(signed_payload, "rb") as in_file: + auth_var = EfiVariableAuthentication2(decodefs=in_file) + + # cert_data contains the PKCS#7 blob carried inside WIN_CERTIFICATE_UEFI_GUID. + signed_data = pkcs7_get_signed_data_structure(auth_var.auth_info.cert_data) + auth_var.auth_info.cert_data = signed_data + + out_path = signed_payload.with_name(signed_payload.name + ".stripped") + with open(out_path, "wb") as out_file: + out_file.write(auth_var.encode()) + + logging.info("Stripped signed payload written to: %s", out_path) + return out_path + + +def main() -> int: + """Parse CLI arguments and strip ContentInfo from the provided payload.""" + parser = argparse.ArgumentParser(description="Strip ContentInfo from signed payload") + parser.add_argument("signed_payload", type=pathlib.Path, help="Path to signed payload") + args = parser.parse_args() + + try: + strip_content_info(args.signed_payload) + except Exception as error: + logging.error("Failed to strip ContentInfo: %s", error) + return 1 + + return 0 + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") + sys.exit(main()) diff --git a/scripts/validate_kek.py b/scripts/validate_kek.py index 36a2fb6..acd815d 100644 --- a/scripts/validate_kek.py +++ b/scripts/validate_kek.py @@ -12,6 +12,9 @@ from datetime import datetime, timezone from pathlib import Path +from pyasn1.codec.der.decoder import decode as der_decode +from pyasn1_modules import rfc2315 + # Import validation functions from auth_var_tool sys.path.insert(0, str(Path(__file__).parent)) # Import the verify function from auth_var_tool @@ -27,6 +30,25 @@ EXPECTED_PAYLOAD_HASH = "5b85333c009d7ea55cbb6f11a5c2ff45ee1091a968504c929aed25c84674962f" +def has_content_info_wrapper(cert_data: bytes) -> bool: + """Return True if cert_data is a PKCS#7 ContentInfo(signedData) envelope. + + EDK2 firmware historically expects raw SignedData in WIN_CERTIFICATE_UEFI_GUID.CertData. + A ContentInfo outer SEQUENCE was not supported by EDK2 until recently: + https://github.com/microsoft/mu_tiano_plus/commit/37d3eb026a766b2405daae47e02094c2ec248646 + + Submitting a file with a ContentInfo wrapper may cause authentication failures on + older firmware. + """ + try: + content_info, remainder = der_decode(cert_data, asn1Spec=rfc2315.ContentInfo()) + if remainder: + return False + return content_info.getComponentByName("contentType") == rfc2315.signedData + except Exception: + return False + + def validate_single_kek( kek_file: Path, quiet: bool = False @@ -47,6 +69,7 @@ def validate_single_kek( "path": str(kek_file), "valid": False, "payload_hash_valid": False, + "content_info_wrapped": False, "error": None, "warnings": [], "details": {} @@ -70,6 +93,15 @@ def validate_single_kek( logging.warning(f" Expected: {EXPECTED_PAYLOAD_HASH}") logging.warning(f" Got: {payload_hash}") + # Check for ContentInfo wrapper in cert_data + file_result["content_info_wrapped"] = has_content_info_wrapper(auth_var.auth_info.cert_data) + if file_result["content_info_wrapped"]: + warning_msg = ( + "cert_data contains a PKCS#7 ContentInfo wrapper. " + ) + file_result["warnings"].append(warning_msg) + logging.warning(" [!] ContentInfo wrapper detected in cert_data!") + # Validate the file using auth_var_tool.verify_variable # Create a namespace object with the required arguments verify_args = argparse.Namespace( @@ -183,6 +215,7 @@ def validate_kek_folder( "path": str(bin_file), "valid": False, "payload_hash_valid": False, + "content_info_wrapped": False, "error": None, "warnings": [], "details": {} @@ -206,6 +239,15 @@ def validate_kek_folder( logging.warning(f" Expected: {EXPECTED_PAYLOAD_HASH}") logging.warning(f" Got: {payload_hash}") + # Check for ContentInfo wrapper in cert_data + file_result["content_info_wrapped"] = has_content_info_wrapper(auth_var.auth_info.cert_data) + if file_result["content_info_wrapped"]: + warning_msg = ( + "cert_data contains a PKCS#7 ContentInfo wrapper." + ) + file_result["warnings"].append(warning_msg) + logging.warning(" [!] ContentInfo wrapper detected in cert_data!") + # Validate the file using auth_var_tool.verify_variable # Create a namespace object with the required arguments verify_args = argparse.Namespace(