File shim-posttrans.spec of Package shim

#
# spec file for package pesign-obs-integration
#
# Copyright (c) 2025 SUSE LLC
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
# upon. The license for this file, and modifications and additions to the
# file, is the same license as for the pristine package itself (unless the
# license for the pristine package is not an Open Source License, in which
# case the license is the MIT License). An "Open Source License" is a
# license that conforms to the Open Source Definition (Version 1.9)
# published by the Open Source Initiative.

# Please submit bugfixes or comments via https://bugs.opensuse.org/
#
# needssslcertforbuild

Name:           shim-posttrans
Version:        0.1
Release:        0
Summary:        Testing lua script for shim posttrans
License:        GPL-2.0-or-later
Group:          Development/Tools/Other
# DER, be casefule the priority, the following global macro also must be changed
Source0:	Microsoft_Corporation_UEFI_CA_2011.der
Source1:	Microsoft_UEFI_CA_2023.crt
Source2:	openSUSE_Secure_Boot_CA_2013.crt
Source3:	SUSE_Linux_Enterprise_Secure_Boot_CA_2013.crt
BuildRequires:  openssl >= 0.9.8
BuildRequires:  vim

# subject hash of openSUSE/SLE/devel certificates for identifying devel project
%global prjsubjec_hash %(test -f %{_sourcedir}/_projectcert.crt && openssl x509 -in %{_sourcedir}/_projectcert.crt -inform PEM -noout -subject_hash 2>/dev/null || echo "PRJ_SUBJECT_NOT_FOUND")
%global prjissuer_hash %(test -f %{_sourcedir}/_projectcert.crt && openssl x509 -in %{_sourcedir}/_projectcert.crt -inform PEM -noout -issuer_hash 2>/dev/null || echo "PRJ_ISSUER_NOT_FOUND")
%global opensusesubject_hash %(openssl x509 -in %{SOURCE2} -inform DER -noout -subject_hash 2>/dev/null)
# 應該全轉 DER, 除 _projectcrt.crt 以外
%global slessubject_hash %(openssl x509 -in %{SOURCE3} -inform DER -noout -subject_hash 2>/dev/null)

# Hex content (DER format) will be used in the TARGET_CERT_HEXES array in Lua pretrans script 
%global microsoft_ca_hex %(xxd -p %{SOURCE0} | tr -d '\\n')
%global microsoft_ca_2023_hex %(xxd -p %{SOURCE1} | tr -d '\\n')
%global opensuse_ca_hex %(xxd -p %{SOURCE2} | tr -d '\\n')
%global sles_ca_hex %(xxd -p %{SOURCE3} | tr -d '\\n')
%global prjcert_hex %(test -f %{_sourcedir}/_projectcert.crt && (openssl x509 -in %{_sourcedir}/_projectcert.crt -outform DER -out - | xxd -p | tr -d '\\n') 2>/dev/null)

# subject hash of openSUSE/SLE/devel certificates for identifying devel project
%global prjsubjec_hash %(test -f %{_sourcedir}/_projectcert.crt && openssl x509 -in %{_sourcedir}/_projectcert.crt -inform PEM -noout -subject_hash 2>/dev/null || echo "PRJ_SUBJECT_NOT_FOUND")
%global prjissuer_hash %(test -f %{_sourcedir}/_projectcert.crt && openssl x509 -in %{_sourcedir}/_projectcert.crt -inform PEM -noout -issuer_hash 2>/dev/null || echo "PRJ_ISSUER_NOT_FOUND")
%global opensusesubject_hash %(openssl x509 -in %{SOURCE11} -inform DER -noout -subject_hash 2>/dev/null)
%global slessubject_hash %(openssl x509 -in %{SOURCE12} -inform DER -noout -subject_hash 2>/dev/null)
# Hex content of certs (DER format) will be used in the TARGET_CERT_HEXES array in pretrans script
%global opensuse_ca_hex %(xxd -p %{SOURCE11} | tr -d '\\n')
%global sles_ca_hex %(xxd -p %{SOURCE12} | tr -d '\\n')
%global microsoft_ca_hex %(xxd -p %{SOURCE0} | tr -d '\\n')
%global microsoft_ca_2023_hex %(xxd -p %{SOURCE1} | tr -d '\\n')
%global prjcert_hex %(test -f %{_sourcedir}/_projectcert.crt && (openssl x509 -in %{_sourcedir}/_projectcert.crt -outform DER -out - | xxd -p | tr -d '\\n') 2>/dev/null)


%description
Testing lua script for shim posttrans

%prep
echo "DEBUG: prjsubjec_hash is: %{prjsubjec_hash}"
echo "DEBUG: opensusesubject_hash is: %{opensusesubject_hash}"
echo "DEBUG: slessubject_hash is: %{slessubject_hash}"

%build

%install
# install MS certificate hex, just for packaging for testing
install -d %{buildroot}/%{_sysconfdir}/uefi/certs/
install -m 644 %{SOURCE0} %{buildroot}/%{_sysconfdir}/uefi/certs/

# The pretrans script only checks for necessary certificates in the uefi db for
# openSUSE/SLE repositories. This 'if' statement prevents the checking logic
# from blocking the build process during installation testing on devel/staging projects
%if "%{prjissuer_hash}" != "%{opensusesubject_hash}" && "%{prjissuer_hash}" != "%{slessubject_hash}"
%pretrans -p <lua>
-- Using Lua
print("INFO: Current Lua Version: " .. tostring(_VERSION))

-- ==========================================================================================
-- This pretrans script verifies that the UEFI db should have the necessary certificate to
-- allow the shim binary to boot.
-- The installation will be aborted if the db is missing the target certificate. To proceed,
-- the user must enroll the target certificate in the db or disable UEFI Secure Boot. 
-- ==========================================================================================

local db_filename = "/sys/firmware/efi/efivars/db-d719b2cb-3d3a-4596-a3bc-dad00e67656f"

-- The db file existence check
-- Use pcall to execute rpm.open to prevent errors from being thrown when 
-- the file cannot be found, causing RPM to fail.
local success, result = pcall(rpm.open, db_filename, "rb")

local f_check = nil

if not success then
    -- pcall catches errors (e.g. "No such file or directory")
    print("WARNING: Attempt to open db EFI variable file failed. Error message: " .. tostring(result))
    print("WARNING: This usually means the system is not booted in UEFI mode. Skipping all db check steps.")
    return 0 
else
    -- If pcall succeeds, result may be an archive handle or nil (depending on the behavior of rpm.open)
    f_check = result
    if not f_check then
	-- The archive does not exist, but rpm.open returns nil
        print("WARNING: db EFI variable file does not exist (rpm.open returned nil). Skipping db check steps.")
        return 0
    else
	-- If the file exists and is successfully opened, 
	-- close the handle immediately so that subsequent code can open it again.
        f_check:close()
    end
end

-- ==========================================================================================
-- This is the hardcoded target certificate content used to check for its existence.
-- HEX_CONTENT=$(xxd -p taget_certificate.der | tr -d '\n') && echo "$HEX_CONTENT"
-- ==========================================================================================

-- Only the DER format is supported
local TARGET_CERT_HEXES = {
    -- Certificate #1, Microsoft Corporation UEFI CA 2011
    "%{microsoft_ca_hex}", 	
    -- Certificate #2, Microsoft UEFI CA 2023
    "%{microsoft_ca_2023_hex}", 	
%if "%{prjissuer_hash}" == "%{opensusesubject_hash}"
    -- Certificate #3, openSUSE Secure Boot CA 2013
    "%{opensuse_ca_hex}",
%elif "%{prjissuer_hash}" == "%{slessubject_hash}"
    -- Certificate #3, SUSE Linux Enterprise Secure Boot CA 2013
    "%{sles_ca_hex}",
%elif "%{prjissuer_hash}" == "%{prjsubjec_hash}"
    -- Certificate #3, openSUSE Secure Boot CA 2013
    "%{opensuse_ca_hex}",
    -- Certificate #4, SUSE Linux Enterprise Secure Boot CA 2013
    "%{sles_ca_hex}",
    -- Certificate #5, _projectcert.crt
    "%{prjcert_hex}",
%endif  # prjsubjec_hash check in TARGET_CERT_HEXES
}

-- Check if the TARGET_CERT_HEXES array is empty
if #TARGET_CERT_HEXES == 0 then
    print("INFO: certificate list is empty. Skipping certificate check.")
    -- Exiting safely as the certificate list is empty.
    return 0
else
    -- Check if the Hex string for certificate is valid
    for i, cert_hex in ipairs(TARGET_CERT_HEXES) do
        if #cert_hex % 2 ~= 0 then
            print("Error: The length of hard-coded hex string for certificate #" .. i .. " must be an even number.")
            error("The Hex string is invalid. The transaction is being aborted in the pretrans script.")
        end
    end
end

-- =========================================================================
-- Helper functions
-- =========================================================================

-- Convert hexadecimal string to original binary string
local function hex_to_binary(hex)
    local binary = ""
    for i = 1, #hex, 2 do
        local byte_hex = hex:sub(i, i + 1)
        binary = binary .. string.char(tonumber(byte_hex, 16))
    end
    return binary
end

-- =========================================================================
-- Main logic for checking if the db has any target certificate
-- =========================================================================

-- Read existing db contents
local db_content = ""
do  
    -- The db file is now confirmed to exist, open it again to read the contents
    local f = rpm.open(db_filename, "rb")
    
    if f then
        local chunks = {}
        local CHUNK_SIZE = 4096
        local raw_content = ""
        local chunk = f:read(CHUNK_SIZE) 
        
        while chunk do
	    -- If an empty string is read, it means EOF has been reached and the loop is exited.
            if chunk == "" then
                break
            end
            table.insert(chunks, chunk)
            chunk = f:read(CHUNK_SIZE)
        end
        
        raw_content = table.concat(chunks)
        
        f:close()
        
	-- Skip the first 4 bytes (EFI attributes)
        if #raw_content > 4 then
	    -- Truncate from the 5th byte to the end
            db_content = string.sub(raw_content, 5)
	    print("INFO: Successfully read existing db content")
        else
	    -- The file is too small or only has attributes, so it is considered blank.
            db_content = ""
            print("WARNING: db file content length is abnormal (<= 4 bytes). Treated as blank.")
        end
    end
end

-- Check all target certificates
for i, cert_hex in ipairs(TARGET_CERT_HEXES) do

    local target_binary_content = hex_to_binary(cert_hex)

    -- Perform binary string matching
    local start_pos, end_pos = db_content:find(target_binary_content, 1, true)

    if start_pos then
        -- Success: Certificate exist in db
        -- Return 0 to allow the RPM transaction to continue
        print("Target certificate #" .. i .. " was found in the db variable. Proceed with install.")
        return 0
    end
end

-- Certificate not present in db
print("WARNING: The target certificate binary was not found in the db variable.")
print("Please add the appropriate certificate to the db or disable UEFI secure boot.")

-- Secure Boot status check: We only proceed with installation if the certificate is not present in the db and Secure Boot is disabled.
local sb_filename = "/sys/firmware/efi/efivars/SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c"

local success_sb, result_sb = pcall(rpm.open, sb_filename, "rb")

if not success_sb or not result_sb then
    -- If the file is missing, it typically means the system is not UEFI, or Secure Boot is disabled/the variable is absent.
    print("WARNING: SecureBoot EFI variable file does not exist. Proceed with install.")
else
    local f_sb = result_sb
    local raw_content_sb = ""
    local sb_status = 0

    -- Read file contents 
    local chunk_sb = f_sb:read(4096)
    while chunk_sb do
        if chunk_sb == "" then break end
        raw_content_sb = raw_content_sb .. chunk_sb
        chunk_sb = f_sb:read(4096)
    end
    f_sb:close()

    -- SecureBoot status check
    if #raw_content_sb >= 5 then
	-- Skip the first 4-byte attribute header and read the 5th byte (status byte)
        sb_status = string.byte(raw_content_sb, 5)

        if sb_status == 0x00 then
            print("INFO: Since Secure Boot is DISABLED, proceed with install.")
            return 0
        elseif sb_status == 0x01 then
	    error("Fatal error: Secure Boot is ENABLED (status = 0x01), but the target certificate was not found in the db. Aborting installation.")
        else
            error("Fatal error: Secure Boot status is unrecognized (0x" .. string.format("%02x", sb_status) .. "). Aborting installation.")
        end
    else
	error("Fatal error: SecureBoot variable content is too short to determine status. Aborting installation.")
    end
end
%endif  # prjissuer_hash check for equals to opensusesubject_hash or slessubject_hash 

%files
%dir %{_sysconfdir}/uefi/
%dir %{_sysconfdir}/uefi/certs/
%{_sysconfdir}/uefi/certs/*.der

%changelog
openSUSE Build Service is sponsored by