Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions .github/scripts/capsule-versioning-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#!/bin/bash

# This script verifies that defconfig options related to capsule versions are
# properly updated between releases. It expects tag of the new release/RC as
# its only parameter.

set -eu

# note that this works only for up to one release back in history
function get_previous_release() {
local tag=$1

local tag_prefix
tag_prefix=$(echo "$tag" | grep -o '.*_v[0-9]')
if [ "$tag" = "$tag_prefix" ]; then
echo "error: failed to determine prefix of tag: '$tag'." 1>&2
exit 1
fi
tag_prefix=${tag_prefix::-1}

local pos
if [[ $tag =~ .*-rc[0-9]+$ ]]; then
# last non-RC release is the predecessor
pos=1
else
# the previous non-RC release is the predecessor
pos=2
fi

git tag --list "$tag_prefix*" --sort=-version:refname |
grep -v '.*-rc[0-9]\+$' |
sed -n "${pos}p"
}

function uses_v1_capsules() {
local config=$1
echo "$config" | grep -qFx "CONFIG_DRIVERS_EFI_UPDATE_CAPSULES=y" &&
! echo "$config" | grep -qFx "CONFIG_EDK2_CAPSULES_V2=y"
}

function uses_v2_capsules() {
local config=$1
echo "$config" | grep -qFx "CONFIG_EDK2_CAPSULES_V2=y"
}

function transitions_to_v2() {
local config=$1
echo "$config" | grep -qFx "CONFIG_EDK2_CAPSULES_V2_TRANSITION=y"
}

if [ $# -ne 1 ]; then
echo "Usage: $(basename "$0") -h|--help|new-tag"
exit 1
fi

if [ $1 = "-h" ] || [ $1 = "--help" ]; then
echo "Usage: $(basename "$0") -h|--help|new-tag"
exit 0
fi

new_tag=$1

old_tag=$(get_previous_release "$new_tag")
if [ -z "$old_tag" ]; then
echo "warning: no previous release for '$new_tag', skipping checks." 1>&2
exit 0
fi
echo "info: determined '$old_tag' to be the predecessor of '$new_tag'."

fail=0

# check all configs to not figure out mapping tag name to a config name, some
# false-positives are possible due to overlapping releases, but that should be
# rare and obvious to the user
mapfile -t configs < <(grep -l 'EFI_UPDATE_CAPSULE' configs/config.*)
for config in "${configs[@]}"; do
new_config=$(git show "$new_tag:$config")
old_config=$(git show "$old_tag:$config")
Comment thread
filipleple marked this conversation as resolved.

# check that `CONFIG_EDK2_CAPSULES_V2=y` is not added to
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could also check if the transition is not used without CONFIG_EDK2_CAPSULES_V2.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That can't happen because

config EDK2_CAPSULES_V2_TRANSITION
bool "Capsules Update: transitioning from v1 to v2"
default n
depends on EDK2_CAPSULES_V2

and this script probably shouldn't validate state of Kconfig files.

# `CONFIG_DRIVERS_EFI_UPDATE_CAPSULES=y` without
# `CONFIG_EDK2_CAPSULES_V2_TRANSITION=y`
if uses_v1_capsules "$old_config" &&
Copy link
Copy Markdown
Contributor

@philipanda philipanda Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is CONFIG_DRIVERS_EFI_UPDATE_CAPSULES supposed to be disabled on full, non-transition V2 releases?
The checks don't allow CONFIG_DRIVERS_EFI_UPDATE_CAPSULES=y && CONFIG_EDK2_CAPSULES_V2=y after the transition is finished.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure which combination of options you think is problematic. CONFIG_DRIVERS_EFI_UPDATE_CAPSULES=y will always be there when capsules are enabled. uses_v1_capsules() returns non-zero for a config with V2 capsules, so this condition will not be entered.

uses_v2_capsules "$new_config" &&
! transitions_to_v2 "$new_config"; then
echo "error: '$config' switches to V2 without CONFIG_EDK2_CAPSULES_V2_TRANSITION=y" 1>&2
fail=1
fi

# check that `CONFIG_EDK2_CAPSULES_V2_TRANSITION=y` doesn't live longer than
# one release cycle
if transitions_to_v2 "$old_config" &&
transitions_to_v2 "$new_config"; then
echo "error: '$config' didn't remove CONFIG_EDK2_CAPSULES_V2_TRANSITION=y after a release" 1>&2
fail=1
fi

# check that `CONFIG_EDK2_CAPSULES_V2_TRANSITION=y` is not added to
# `CONFIG_EDK2_CAPSULES_V2=y`
if uses_v2_capsules "$old_config" &&
uses_v2_capsules "$new_config" &&
transitions_to_v2 "$new_config"; then
echo "error: '$config' adds CONFIG_EDK2_CAPSULES_V2_TRANSITION=y to V2" 1>&2
fail=1
fi

# check that `CONFIG_EDK2_CAPSULES_V2_TRANSITION=y` is not removed causing a
# downgrade to V2
if uses_v2_capsules "$old_config" &&
uses_v1_capsules "$new_config"; then
echo "error: '$config' removes CONFIG_EDK2_CAPSULES_V2=y" 1>&2
fail=1
fi
done

exit "$fail"
2 changes: 1 addition & 1 deletion .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
check-upstream-status:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
Comment thread
filipleple marked this conversation as resolved.
with:
# Checkout pull request HEAD commit instead of merge commit
# See: https://github.com/actions/checkout#checkout-pull-request-head-commit-instead-of-merge-commit
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/tag-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,6 @@ jobs:
echo "::error::Tag '${tag}' is NOT signed. Create it with: git tag -s"
exit 1
fi

- name: Verify update capsules support in defconfigs
run: .github/scripts/capsule-versioning-check.sh "${GITHUB_REF_NAME}"
154 changes: 134 additions & 20 deletions capsule.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ function print_usage() {
echo ' -o subroot-certificate-file'
echo ' -s signing-certificate-file'
echo ' -b (the flag adds battery check DXE into the capsule)'
echo ' resign resign an existing capsule with a different key'
echo ' -t root-certificate-file'
echo ' -o subroot-certificate-file'
echo ' -s signing-certificate-file'
echo ' positional arguments: input-capsule output-capsule'
echo ' create_cabinet create a fwupd cabinet (.cab) from a capsule'
echo ' positional argument: capsule-file'
echo ' upload_lvfs upload a cabinet (.cab) to LVFS, options'
Expand Down Expand Up @@ -285,7 +290,6 @@ EOF
local opt_sub_cert=$sub_cert
local opt_sign_cert=$sign_cert
if [ "${CONFIG_EDK2_CAPSULES_V2:-n}${CONFIG_EDK2_CAPSULES_V2_TRANSITION:-n}" = yn ]; then
echo "nested capsules"
# The inner capsule is always signed with the test key. Not signing it
# at all doesn't work because FmpDxe doesn't accept unsigned payloads at
# least due to Image->AuthInfo.Hdr.wRevision check in
Expand Down Expand Up @@ -359,6 +363,129 @@ EOF
echo "Created the capsule at '$cap_file'"
}

function assert_file_exists() {
local path=$1

if [ ! -f "$path" ]; then
die "File '$path' not found"
fi
}

function assert_file_is_a_capsule() {
local path=$1

local fmp_guid_bytes_hex=edd5cb6d2de8444cbda17194199ad92a
if [ "$(xxd -l 16 -ps "$path")" != "$fmp_guid_bytes_hex" ]; then
die "'$path' is not an FMP capsule file"
fi
}

function assert_command_exists() {
local cmd=$1

if ! command -v "$cmd" >/dev/null 2>&1; then
die "'$cmd' not found in PATH"
fi
}

function decode_capsule() {
local tmp_dir=$1
local capsule=$2
local -n result=$3

"${edk_tools}/GenerateCapsule" --decode "$capsule" \
--output "$tmp_dir/decoded"

local json_file="$tmp_dir/decoded.json"
result["fw_version"]=$(jq -r '.Payloads[0].FwVersion' "$json_file")
result["guid"]=$(jq -r '.Payloads[0].Guid' "$json_file")
result["lowest_supported_version"]=$(jq -r '.Payloads[0].LowestSupportedVersion' "$json_file")
result["payload"]=$(jq -r '.Payloads[0].Payload' "$json_file")

local drivers
drivers=$(ls -v "$tmp_dir"/decoded.EmbeddedDriver* 2>/dev/null | while read -r f; do
printf ' { "Driver": "%s" },\n' "$f"
done | sed '$s/,$//')
result["drivers"]="$drivers"
}

function encode_capsule() {
local tmp_dir=$1
local cap_out=$2
local -n json_data=$3

cat > "$tmp_dir/cap.json" <<EOF
{
"EmbeddedDrivers": [
${json_data["drivers"]}
],
"Payloads": [
{
"Payload": "${json_data["payload"]}",
"Guid": "${json_data["guid"]}",
"FwVersion": "${json_data["fw_version"]}",
"LowestSupportedVersion": "${json_data["lowest_supported_version"]}",
"OpenSslSignerPrivateCertFile": "${json_data["sign_cert"]}",
"OpenSslOtherPublicCertFile": "${json_data["sub_cert"]}",
"OpenSslTrustedPublicCertFile": "${json_data["root_cert"]}"
}
]
}
EOF

"${edk_tools}/GenerateCapsule" --encode \
--capflag PersistAcrossReset \
--json-file "$tmp_dir/cap.json" \
--output "$cap_out"
}

function resign_subcommand() {
if [ ! -x "${edk_tools}/GenerateCapsule" ]; then
die "'${edk_tools}/GenerateCapsule' can't be executed"
fi

local root_cert sub_cert sign_cert
OPTIND=1
while getopts "t:o:s:" OPTION; do
case $OPTION in
t) root_cert="$OPTARG" ;;
o) sub_cert="$OPTARG" ;;
s) sign_cert="$OPTARG" ;;
*) exit 1 ;;
esac
done
shift $((OPTIND - 1))

if [ $# -ne 2 ]; then
die "Incorrect number of positional parameters: $# (expected: 2)"
fi

local in_capsule=$1
local out_capsule=$2

assert_file_exists "$in_capsule"
assert_file_is_a_capsule "$in_capsule"
if [ -e "$out_capsule" ]; then
confirm "Overwrite already existing '$out_capsule'?"
fi
assert_command_exists jq

local tmp_dir
tmp_dir=$(mktemp --tmpdir --directory --suffix -cap XXXXXXXX)
trap "$(printf 'rm -r -- %q' "$tmp_dir")" EXIT

check_cert root "$root_cert"
check_cert sub "$sub_cert"
check_cert sign "$sign_cert"

local -A metadata
decode_capsule "$tmp_dir" "$in_capsule" metadata
metadata[sign_cert]=$sign_cert
metadata[sub_cert]=$sub_cert
metadata[root_cert]=$root_cert
encode_capsule "$tmp_dir" "$out_capsule" metadata
}

function create_cabinet_subcommand() {
if [ $# -ne 1 ]; then
die "Incorrect number of input parameters specified: $# (expected: 1)"
Expand All @@ -371,18 +498,9 @@ function create_cabinet_subcommand() {
die "No input capsule specified"
fi

if [ ! -f "$capsule" ]; then
die "File $capsule not found"
fi

if ! command -v fwupdtool >/dev/null 2>&1; then
die "fwupdtool not found in PATH"
fi

local fmp_guid_bytes_hex=edd5cb6d2de8444cbda17194199ad92a
if [ "$(xxd -l 16 -ps "$capsule")" != "$fmp_guid_bytes_hex" ]; then
die "'$capsule' is not an FMP capsule file"
fi
assert_file_exists "$capsule"
assert_file_is_a_capsule "$capsule"
assert_command_exists fwupdtool

source_coreboot_config
require_capsule_support
Expand Down Expand Up @@ -572,9 +690,7 @@ function upload_lvfs_subcommand() {
cabinet="$1"
fi

if [ ! -f "$cabinet" ]; then
die "File '$cabinet' not found"
fi
assert_file_exists "$cabinet"

if [ -z "$email" ]; then
die "LVFS email is not set. Put LVFS_EMAIL into '$creds_file' or pass -e"
Expand All @@ -583,9 +699,7 @@ function upload_lvfs_subcommand() {
die "LVFS token is not set. Put LVFS_TOKEN into '$creds_file' or pass -t"
fi

if ! command -v curl >/dev/null 2>&1; then
die "curl not found in PATH"
fi
assert_command_exists curl

url="${base_url%/}/lvfs/upload/token"

Expand Down Expand Up @@ -625,7 +739,7 @@ subcommand=$1
shift

case "$subcommand" in
box|help|keygen|make|create_cabinet|upload_lvfs)
box|help|keygen|make|resign|create_cabinet|upload_lvfs)
"$subcommand"_subcommand "$@" ;;

*)
Expand Down
6 changes: 3 additions & 3 deletions configs/config.msi_ms7d25_ddr4
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
CONFIG_LOCALVERSION="v1.1.5"
CONFIG_LOCALVERSION="v1.1.7-rc1"
CONFIG_OPTION_BACKEND_NONE=y
CONFIG_VENDOR_MSI=y
CONFIG_ONBOARD_VGA_IS_PRIMARY=y
Expand Down Expand Up @@ -26,8 +26,8 @@ CONFIG_PCIEXP_LANE_ERR_STAT_CLEAR=y
CONFIG_DRIVERS_EFI_VARIABLE_STORE=y
CONFIG_DRIVERS_EFI_FW_INFO=y
CONFIG_DRIVERS_EFI_MAIN_FW_GUID="0f3e8ba6-19d3-461b-bbbc-8b5689fe8097"
CONFIG_DRIVERS_EFI_MAIN_FW_VERSION=0x01010580
CONFIG_DRIVERS_EFI_MAIN_FW_LSV=0x01010580
CONFIG_DRIVERS_EFI_MAIN_FW_VERSION=0x01010701
CONFIG_DRIVERS_EFI_MAIN_FW_LSV=0x01010701
CONFIG_DRIVERS_EFI_UPDATE_CAPSULES=y
CONFIG_TPM2=y
CONFIG_BOOTMEDIA_LOCK_CONTROLLER=y
Expand Down
6 changes: 3 additions & 3 deletions configs/config.msi_ms7d25_ddr5
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
CONFIG_LOCALVERSION="v1.1.5"
CONFIG_LOCALVERSION="v1.1.7-rc1"
CONFIG_OPTION_BACKEND_NONE=y
CONFIG_VENDOR_MSI=y
CONFIG_ONBOARD_VGA_IS_PRIMARY=y
Expand Down Expand Up @@ -26,8 +26,8 @@ CONFIG_PCIEXP_LANE_ERR_STAT_CLEAR=y
CONFIG_DRIVERS_EFI_VARIABLE_STORE=y
CONFIG_DRIVERS_EFI_FW_INFO=y
CONFIG_DRIVERS_EFI_MAIN_FW_GUID="119059ec-1a5e-4844-b2e6-6f573b257570"
CONFIG_DRIVERS_EFI_MAIN_FW_VERSION=0x01010580
CONFIG_DRIVERS_EFI_MAIN_FW_LSV=0x01010580
CONFIG_DRIVERS_EFI_MAIN_FW_VERSION=0x01010701
CONFIG_DRIVERS_EFI_MAIN_FW_LSV=0x01010701
CONFIG_DRIVERS_EFI_UPDATE_CAPSULES=y
CONFIG_TPM2=y
CONFIG_BOOTMEDIA_LOCK_CONTROLLER=y
Expand Down
6 changes: 3 additions & 3 deletions configs/config.msi_ms7e06_ddr4
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
CONFIG_LOCALVERSION="v0.9.3"
CONFIG_LOCALVERSION="v0.9.5-rc1"
CONFIG_OPTION_BACKEND_NONE=y
CONFIG_VENDOR_MSI=y
CONFIG_ONBOARD_VGA_IS_PRIMARY=y
Expand Down Expand Up @@ -26,8 +26,8 @@ CONFIG_PCIEXP_LANE_ERR_STAT_CLEAR=y
CONFIG_DRIVERS_EFI_VARIABLE_STORE=y
CONFIG_DRIVERS_EFI_FW_INFO=y
CONFIG_DRIVERS_EFI_MAIN_FW_GUID="91e6522e-25e1-4543-9b38-02e2aeafc9ed"
CONFIG_DRIVERS_EFI_MAIN_FW_VERSION=0x00090380
CONFIG_DRIVERS_EFI_MAIN_FW_LSV=0x00090380
CONFIG_DRIVERS_EFI_MAIN_FW_VERSION=0x00090501
CONFIG_DRIVERS_EFI_MAIN_FW_LSV=0x00090501
CONFIG_DRIVERS_EFI_UPDATE_CAPSULES=y
CONFIG_TPM2=y
CONFIG_BOOTMEDIA_LOCK_CONTROLLER=y
Expand Down
Loading
Loading