From baf745e41a7efc19ccef19a87db7af1d9b428589 Mon Sep 17 00:00:00 2001 From: David Frank Date: Thu, 25 Jun 2026 16:24:29 +0200 Subject: [PATCH 1/6] feat: Implement new bazel rules for building SEV recovery GuestOS images MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces the build infrastructure for SEV recovery GuestOS images. These images are recovery images whose measured boot components (kernel, initrd, OVMF, boot args) are taken from a previously released GuestOS version and whose launch measurements are verified against an NNS-signed `BlessAlternativeGuestOsVersion` proposal at build time. The signed proposal is embedded in the boot partition (and later verified at boot time). ## Build usage ```bash ALTERNATIVE_GUESTOS_BASE_VERSION= \ ALTERNATIVE_GUESTOS_PROPOSAL_ID= \ bazel build //ic-os/guestos/envs/sev-recovery:update-img.tar.zst ``` ## Changes ### New crates - **`rs/ic_os/alternative_guestos`** — Extracts the existing proposal verification logic (`read_and_verify_signed_bless_alternative_guest_os_version_proposal`) out of `open_rootfs` into a reusable library. The `nns_public_key_override` parameter is now always present (previously conditionally compiled), with `None` passed in production builds so the hardcoded NNS public key is used. - **`rs/ic_os/build_tools/alternative_guestos`** — A build tool (`alternative_guestos_proposal_tool`) with two subcommands: - `download-signed-proposal`: fetches a certified `get_proposal_info` response from the NNS governance canister via ic-agent and stores the CBOR certificate to disk, verifying it immediately. - `validate-measurements`: checks that locally generated launch measurements overlap with the measurements blessed in the proposal. ### Bazel build rules (`ic-os/alternative_guestos.bzl`) - `download_alternative_guestos_proposal` — downloads and verifies a signed proposal, driven by the `ALTERNATIVE_GUESTOS_PROPOSAL_ID` env var. - `prepare_alternative_guestos_base_bootfs_tree_tar` — downloads a released GuestOS update image, extracts its boot partition via fuse2fs, and produces a tarball of the boot file tree. Driven by `ALTERNATIVE_GUESTOS_BASE_VERSION`. - `validate_launch_measurements_match` — runs the measurement-overlap check against the downloaded proposal. ### Build wiring (`ic-os/defs.bzl`) - Adds a `build_alternative_guestos_image` flag to `icos_build`. When enabled, the boot partition is built from the downloaded base bootfs tree (plus the embedded proposal), the boot args are reused from the base release, and the launch measurements are validated against the proposal. - Refactors boot partition file extraction (`extract_boot_partition_files`) to extract initrd, vmlinuz, OVMF, and boot args directly from the built boot partition image via `debugfs`, so launch measurements reflect the actual partition contents. --- Cargo.lock | 33 +++ Cargo.toml | 2 + bazel/BUILD.bazel | 12 ++ bazel/workspace_status.sh | 7 + ic-os/alternative_guestos.bzl | 97 +++++++++ ic-os/defs.bzl | 192 +++++++++++++----- ic-os/guestos/envs/sev-recovery/BUILD.bazel | 24 +++ rs/ic_os/alternative_guestos/BUILD.bazel | 18 ++ rs/ic_os/alternative_guestos/Cargo.toml | 13 ++ rs/ic_os/alternative_guestos/src/lib.rs | 1 + .../src/proposal.rs | 7 +- .../alternative_guestos/BUILD.bazel | 28 +++ .../alternative_guestos/Cargo.toml | 19 ++ .../alternative_guestos/src/download.rs | 50 +++++ .../alternative_guestos/src/main.rs | 62 ++++++ .../alternative_guestos/src/proposal_build.rs | 129 ++++++++++++ .../tool/src/guestos/bootstrap_ic_node.rs | 1 - rs/ic_os/open_rootfs/BUILD.bazel | 5 +- rs/ic_os/open_rootfs/Cargo.toml | 1 + rs/ic_os/open_rootfs/src/main.rs | 1 - rs/ic_os/open_rootfs/src/recovery.rs | 5 +- 21 files changed, 643 insertions(+), 64 deletions(-) create mode 100644 ic-os/alternative_guestos.bzl create mode 100644 ic-os/guestos/envs/sev-recovery/BUILD.bazel create mode 100644 rs/ic_os/alternative_guestos/BUILD.bazel create mode 100644 rs/ic_os/alternative_guestos/Cargo.toml create mode 100644 rs/ic_os/alternative_guestos/src/lib.rs rename rs/ic_os/{open_rootfs => alternative_guestos}/src/proposal.rs (94%) create mode 100644 rs/ic_os/build_tools/alternative_guestos/BUILD.bazel create mode 100644 rs/ic_os/build_tools/alternative_guestos/Cargo.toml create mode 100644 rs/ic_os/build_tools/alternative_guestos/src/download.rs create mode 100644 rs/ic_os/build_tools/alternative_guestos/src/main.rs create mode 100644 rs/ic_os/build_tools/alternative_guestos/src/proposal_build.rs diff --git a/Cargo.lock b/Cargo.lock index 6edfb8d066e8..f81308557a5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -382,6 +382,25 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "alternative_guestos_tool" +version = "0.0.0" +dependencies = [ + "anyhow", + "candid", + "clap 4.6.0", + "ic-agent", + "ic-certification 3.1.0", + "ic-nns-constants", + "ic-nns-governance-api", + "ic-nns-governance-conversions", + "ic-protobuf", + "ic_os_alternative_guestos", + "serde_cbor", + "serde_json", + "tokio", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -16304,6 +16323,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "ic_os_alternative_guestos" +version = "0.0.0" +dependencies = [ + "anyhow", + "candid", + "ic-certificate-verification", + "ic-certification 3.1.0", + "ic-nns-constants", + "ic-nns-governance-api", + "serde_cbor", +] + [[package]] name = "ic_os_logging" version = "0.0.0" @@ -19144,6 +19176,7 @@ dependencies = [ "ic-nns-governance-api", "ic-types", "ic_device", + "ic_os_alternative_guestos", "ic_os_logging", "linux_kernel_command_line", "pem", diff --git a/Cargo.toml b/Cargo.toml index b8abae7fa090..8ffe07f55f99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -165,6 +165,8 @@ members = [ "rs/ic_os/build_tools/dflate", "rs/ic_os/build_tools/diroid", "rs/ic_os/build_tools/partition_tools", + "rs/ic_os/build_tools/alternative_guestos", + "rs/ic_os/alternative_guestos", "rs/ic_os/command_runner", "rs/ic_os/config/tool", "rs/ic_os/config/types", diff --git a/bazel/BUILD.bazel b/bazel/BUILD.bazel index 4f9c2fa18326..002340e593cc 100644 --- a/bazel/BUILD.bazel +++ b/bazel/BUILD.bazel @@ -60,6 +60,18 @@ string_flag( visibility = ["//visibility:public"], ) +write_stable_status_file_var( + name = "alternative_guestos_proposal_id.txt", + varname = "STABLE_ALTERNATIVE_GUESTOS_PROPOSAL_ID", + visibility = ["//visibility:public"], +) + +write_stable_status_file_var( + name = "alternative_guestos_base_version.txt", + varname = "STABLE_ALTERNATIVE_GUESTOS_BASE_VERSION", + visibility = ["//visibility:public"], +) + write_stable_status_file_var( name = "version.txt", varname = "STABLE_VERSION", diff --git a/bazel/workspace_status.sh b/bazel/workspace_status.sh index 00598c794bde..40ce47ed9230 100755 --- a/bazel/workspace_status.sh +++ b/bazel/workspace_status.sh @@ -21,6 +21,13 @@ else exit 1 fi +if [ -n "${ALTERNATIVE_GUESTOS_PROPOSAL_ID:-}" ]; then + echo "STABLE_ALTERNATIVE_GUESTOS_PROPOSAL_ID ${ALTERNATIVE_GUESTOS_PROPOSAL_ID}" +fi +if [ -n "${ALTERNATIVE_GUESTOS_BASE_VERSION:-}" ]; then + echo "STABLE_ALTERNATIVE_GUESTOS_BASE_VERSION ${ALTERNATIVE_GUESTOS_BASE_VERSION}" +fi + # Used as farm metadata FARM_METADATA="USER=${USER:-${HOSTUSER:-$(whoami)}}" if [ -n "${CI_JOB_NAME:-}" ]; then diff --git a/ic-os/alternative_guestos.bzl b/ic-os/alternative_guestos.bzl new file mode 100644 index 000000000000..93f19fac3acf --- /dev/null +++ b/ic-os/alternative_guestos.bzl @@ -0,0 +1,97 @@ +def _download_alternative_guestos_proposal_impl(ctx): + output = ctx.actions.declare_file(ctx.label.name) + + command = """ +set -euo pipefail + +proposal_id="$(cat {proposal_id_file})" + +if [[ -z "$proposal_id" ]]; then + echo "//{package}:{name} requires ALTERNATIVE_GUESTOS_PROPOSAL_ID to be set in the environment before invoking Bazel." >&2 + exit 1 +fi + +{tool} download-signed-proposal \ + --proposal-id "$proposal_id" \ + --nns-url {nns_url} \ + --output {output} +""".format( + name = ctx.label.name, + nns_url = ctx.attr.nns_url, + output = output.path, + package = ctx.label.package, + proposal_id_file = ctx.file._proposal_id_file.path, + tool = ctx.executable._tool.path, + ) + + ctx.actions.run_shell( + command = command, + inputs = [ctx.file._proposal_id_file], + outputs = [output], + tools = [ctx.attr._tool.files_to_run], + mnemonic = "DownloadAlternativeGuestosProposal", + ) + + return [DefaultInfo(files = depset([output]))] + +download_alternative_guestos_proposal = rule( + implementation = _download_alternative_guestos_proposal_impl, + attrs = { + "nns_url": attr.string(default = "https://ic0.app"), + "_proposal_id_file": attr.label( + allow_single_file = True, + default = Label("//bazel:alternative_guestos_proposal_id.txt"), + ), + "_tool": attr.label( + default = Label("//rs/ic_os/build_tools/alternative_guestos"), + executable = True, + cfg = "exec", + ), + }, +) + +# Creates a tarball containing the bootfs file tree extracted from a released GuestOS. +def prepare_alternative_guestos_base_bootfs_tree_tar(name, out, tags = None, target_compatible_with = None): + native.genrule( + name = name, + srcs = ["//bazel:alternative_guestos_base_version.txt"], + outs = [out], + cmd = """ +set -euo pipefail + +base_version="$$(cat $<)" + +if [[ -z "$$base_version" ]]; then + echo "//{package}:{name} requires ALTERNATIVE_GUESTOS_BASE_VERSION to be set in the environment before invoking Bazel." >&2 + exit 1 +fi + +tmpdir="$$(mktemp -d)" +mounted=0 +cleanup() {{ + set +e + if [[ "$$mounted" -eq 1 ]]; then + fusermount3 -u "$$tmpdir/bootfs" || fusermount -u "$$tmpdir/bootfs" || umount "$$tmpdir/bootfs" + fi + rm -rf "$$tmpdir" +}} +trap cleanup EXIT + +curl --fail --silent --show-error --location \ + "https://download.dfinity.systems/ic/$$base_version/guest-os/update-img/update-img.tar.zst" \ + | tar --extract --zstd --to-stdout --file - boot.img > "$$tmpdir/boot.img" + +mkdir "$$tmpdir/bootfs" +$(location //:fuse2fs) -o ro,norecovery,fakeroot "$$tmpdir/boot.img" "$$tmpdir/bootfs" +mounted=1 +tar --create --file "$@" --numeric-owner -C "$$tmpdir/bootfs" . +""".format( + name = name, + package = native.package_name(), + ), + message = "Downloading alternative GuestOS base boot image and converting it to tar via fuse2fs", + tags = tags, + target_compatible_with = target_compatible_with, + tools = ["//:fuse2fs"], + ) + diff --git a/ic-os/defs.bzl b/ic-os/defs.bzl index fbfdf8e8ad9c..f990c9236900 100644 --- a/ic-os/defs.bzl +++ b/ic-os/defs.bzl @@ -12,6 +12,7 @@ This macro defines the overall build process for ICOS images, including: load("@bazel_skylib//rules:copy_file.bzl", "copy_file") load("@rules_shell//shell:sh_binary.bzl", "sh_binary") load("//bazel:defs.bzl", "zstd_compress") +load("//ic-os:alternative_guestos.bzl", "download_alternative_guestos_proposal", "prepare_alternative_guestos_base_bootfs_tree_tar") load("//ic-os/bootloader:defs.bzl", "build_grub_partition") load("//ic-os/components:defs.bzl", "tree_hash") load("//ic-os/components/conformance_tests:defs.bzl", "component_file_references_test") @@ -22,6 +23,7 @@ def icos_build( image_deps_func, mode = None, malicious = False, + build_alternative_guestos_image = False, upgrades = True, vuln_scan = True, visibility = None, @@ -36,6 +38,7 @@ def icos_build( image_deps_func: Function to be used to generate image manifest mode: dev or prod. If not specified, will use the value of `name` malicious: if True, bundle the `malicious_replica` + build_alternative_guestos_image: if True, build the proposal-aware alternative GuestOS image variant, e.g. for a recovery upgrades: if True, build upgrade images as well vuln_scan: if True, create targets for vulnerability scanning visibility: See Bazel documentation @@ -133,21 +136,43 @@ def icos_build( tags = ["manual"], ) - # Extract initrd and kernel for SEV measurement - tar_extract( - name = "extracted_initrd.img", - src = "rootfs-tree.tar", - path = "boot/initrd.img-*", - wildcards = True, - tags = ["manual"], + prepare_alternative_guestos_base_bootfs_tree_tar( + name = "alternative_guestos_base_bootfs_tree_tar", + out = "alternative_guestos_base_bootfs_tree.tar", + tags = ["manual", "no-cache", "requires-network"], + target_compatible_with = ["@platforms//os:linux"], ) - tar_extract( - name = "extracted_vmlinuz", - src = "rootfs-tree.tar", - path = "boot/vmlinuz-*", - wildcards = True, + # To be 100% correct, this rule should also live below and be duplicated to the default and the test variant but + # since the extracted files are the same in both variants, it's a bit simpler to not duplicate it and only extract + # from the default variant. + native.genrule( + name = "extract_boot_partition_files", + srcs = [":partition-boot.tzst"], + outs = [ + "extracted_boot_args", + "extracted_initrd.img", + "extracted_vmlinuz", + "extracted_OVMF_SEV.fd", + ], + cmd = """ + tmpdir="$$(mktemp -d)" + trap 'rm -rf "$$tmpdir"' EXIT + + tar --extract -a --file "$<" --directory "$$tmpdir" + + # initrd and vmlinuz are symlinks, first extract the symlink targets + initrd_target="$$(/usr/sbin/debugfs -R "stat /initrd.img" "$$tmpdir/partition.img" | sed -n 's/^Fast link dest: "\\(.*\\)"$$/\\1/p')" + vmlinuz_target="$$(/usr/sbin/debugfs -R "stat /vmlinuz" "$$tmpdir/partition.img" | sed -n 's/^Fast link dest: "\\(.*\\)"$$/\\1/p')" + + /usr/sbin/debugfs -R "dump /boot_args $(@D)/extracted_boot_args" "$$tmpdir/partition.img" + /usr/sbin/debugfs -R "dump /OVMF_SEV.fd $(@D)/extracted_OVMF_SEV.fd" "$$tmpdir/partition.img" + /usr/sbin/debugfs -R "dump /$$initrd_target $(@D)/extracted_initrd.img" "$$tmpdir/partition.img" + /usr/sbin/debugfs -R "dump /$$vmlinuz_target $(@D)/extracted_vmlinuz" "$$tmpdir/partition.img" + """, + message = "Extracting files from boot partition", tags = ["manual"], + target_compatible_with = ["@platforms//os:linux"], ) # -------------------- Extract root and boot partitions -------------------- @@ -199,29 +224,43 @@ def icos_build( tags = ["manual", "no-cache"], ) - ext4_image( - name = partition_boot_tzst, - src = ":rootfs-tree.tar", - file_contexts = ":file_contexts", - partition_size = image_deps["bootfs_size"], - subdir = "boot", - target_compatible_with = ["@platforms//os:linux"], - extra_files = { - k: v - for k, v in ( - image_deps["bootfs"].items() + [ - (version_txt, "/version.txt:0644"), - (boot_args, "/boot_args:0644"), - (image_deps["grub_config"], "/grub.cfg:0644"), - ] - ) - }, - tags = ["manual", "no-cache"], - ) + if build_alternative_guestos_image: + ext4_image( + name = partition_boot_tzst, + src = ":alternative_guestos_base_bootfs_tree_tar", + file_contexts = ":file_contexts", + partition_size = image_deps["bootfs_size"], + target_compatible_with = ["@platforms//os:linux"], + extra_files = { + ":alternative_guestos_proposal.cbor": "/alternative_guestos_proposal.cbor:0644", + }, + tags = ["manual", "no-cache"], + ) + else: + ext4_image( + name = partition_boot_tzst, + src = ":rootfs-tree.tar", + file_contexts = ":file_contexts", + partition_size = image_deps["bootfs_size"], + subdir = "boot", + target_compatible_with = ["@platforms//os:linux"], + extra_files = { + k: v + for k, v in ( + image_deps["bootfs"].items() + [ + (version_txt, "/version.txt:0644"), + (boot_args, "/boot_args:0644"), + (image_deps["grub_config"], "/grub.cfg:0644"), + ] + ) + }, + tags = ["manual", "no-cache"], + ) - # The kernel command line (boot args) is generated from boot_args_template: + # The kernel command line (boot args) is usually generated from boot_args_template: # - For OS requiring root signing: Template includes ROOT_HASH placeholder that gets substituted with dm-verity hash # - For OS not requiring root signing: Template is used as-is without ROOT_HASH substitution + # - For alternative GuestOS image builds: The base release boot_args file is reused as-is # # This provides: # - Consistent boot argument handling across all OS types @@ -230,15 +269,19 @@ def icos_build( if image_deps.get("requires_root_signing", False): # Sign the root partition and substitute ROOT_HASH in boot args + signed_partition_root_hash = partition_root_hash + if build_alternative_guestos_image: + signed_partition_root_hash = partition_root_hash + ".local" + native.genrule( name = "generate-" + partition_root_signed_tzst, testonly = malicious, srcs = [partition_root_unsigned_tzst], - outs = [partition_root_signed_tzst, partition_root_hash], + outs = [partition_root_signed_tzst, signed_partition_root_hash], cmd = "$(location //toolchains/sysimage:proc_wrapper) " + "$(location //toolchains/sysimage:verity_sign) " + "-i $< -o $(location :" + partition_root_signed_tzst + ") " + - "-r $(location " + partition_root_hash + ") " + + "-r $(location " + signed_partition_root_hash + ") " + "--dflate $(location //rs/ic_os/build_tools/dflate) " + "--zstd $(location @zstd//:zstd_cli)", executable = False, @@ -251,20 +294,28 @@ def icos_build( tags = ["manual", "no-cache"], visibility = ["//rs/tests:__subpackages__", "//ic-os:__subpackages__"], ) - native.genrule( - name = "generate-" + boot_args, - outs = [boot_args], - srcs = [partition_root_hash, ":boot_args_template"], - cmd = "sed -e s/ROOT_HASH/$$(cat $(location " + partition_root_hash + "))/ " + - "< $(location :boot_args_template) > $@", - tags = ["manual"], - ) + + if build_alternative_guestos_image: + native.alias( + name = boot_args, + actual = ":extracted_boot_args", + tags = ["manual"], + ) + else: + native.genrule( + name = "generate-" + boot_args, + outs = [boot_args], + srcs = [partition_root_hash, ":boot_args_template"], + cmd = "sed -e s/ROOT_HASH/$$(cat $(location " + partition_root_hash + "))/ " + + "< $(location :boot_args_template) > $@", + tags = ["manual"], + ) else: # No signing required, no ROOT_HASH substitution native.alias(name = partition_root_signed_tzst, actual = partition_root_unsigned_tzst, tags = ["manual", "no-cache"]) native.alias( name = boot_args, - actual = ":boot_args_template", + actual = ":extracted_boot_args" if build_alternative_guestos_image else ":boot_args_template", tags = ["manual"], ) @@ -280,22 +331,26 @@ def icos_build( # Supported CPU types for sev-snp-measure, one per AMD EPYC generation. vcpu_types = ["EPYC-v4", "EPYC-Genoa", "EPYC-Turin"] - native.genrule( - name = "generate-" + launch_measurements, - outs = [launch_measurements], - srcs = ["//ic-os/components/ovmf:ovmf_sev", boot_args, ":extracted_initrd.img", ":extracted_vmlinuz"], - visibility = visibility, - tools = ["//ic-os:sev-snp-measure"], - tags = ["manual"], - cmd = r""" - source $(execpath """ + boot_args + """) + # Build the genrule inputs and command conditionally. When building an + # alternative GuestOS image (e.g. for recovery), the generated measurements + # are validated against the proposal *in the same genrule* so that the + # validation can never become a leaf target that is silently skipped. + measurement_srcs = [ + ":extracted_OVMF_SEV.fd", + ":extracted_boot_args", + ":extracted_initrd.img", + ":extracted_vmlinuz", + ] + measurement_tools = ["//ic-os:sev-snp-measure"] + measurement_cmd = r""" + source $(execpath :extracted_boot_args) # Create GuestLaunchMeasurements JSON for each CPU generation, vCPU count, and boot slot (for vcpu_type in """ + " ".join(vcpu_types) + """; do for vcpus in """ + vcpu_configs + """; do # Note: We only create launch measurements for the TEE boot arg variants # (BOOT_ARGS_TEE_A and BOOT_ARGS_TEE_B) for cmdline in "$$BOOT_ARGS_TEE_A" "$$BOOT_ARGS_TEE_B"; do - hex=$$($(execpath //ic-os:sev-snp-measure) --mode snp --vcpus $$vcpus --ovmf "$(execpath //ic-os/components/ovmf:ovmf_sev)" --vcpu-type "$$vcpu_type" --append "$$cmdline" --initrd "$(location extracted_initrd.img)" --kernel "$(location extracted_vmlinuz)") + hex=$$($(execpath //ic-os:sev-snp-measure) --mode snp --vcpus $$vcpus --ovmf "$(execpath extracted_OVMF_SEV.fd)" --vcpu-type "$$vcpu_type" --append "$$cmdline" --initrd "$(location extracted_initrd.img)" --kernel "$(location extracted_vmlinuz)") # Convert hex string to decimal list, e.g. "abcd" -> 171\\n205 measurement=$$(echo -n "$$hex" | fold -w2 | sed "s/^/0x/" | xargs printf "%d\n") jq -na --arg cmd "$$cmdline" --arg m "$$measurement" --arg vcpu_type "$$vcpu_type" '{ @@ -305,7 +360,31 @@ def icos_build( done done done) | jq -sc "{guest_launch_measurements: .}" > $@ - """, + """ + + # When building an alternative GuestOS image, verify that the generated launch measurements correspond + # to those from the proposal. We keep this inside the same build rule, otherwise the check could be skipped + # by Bazel. + if build_alternative_guestos_image: + measurement_srcs.append(":alternative_guestos_proposal.cbor") + measurement_tools.append( + "//rs/ic_os/build_tools/alternative_guestos", + ) + measurement_cmd += r""" + # Validate the generated measurements against the alternative GuestOS proposal + $(execpath //rs/ic_os/build_tools/alternative_guestos) validate-measurements \ + --proposal-path $(location :alternative_guestos_proposal.cbor) \ + --local-measurements-path $@ + """ + + native.genrule( + name = "generate-" + launch_measurements, + outs = [launch_measurements], + srcs = measurement_srcs, + visibility = visibility, + tools = measurement_tools, + tags = ["manual"], + cmd = measurement_cmd, ) component_file_references_test( @@ -322,6 +401,11 @@ def icos_build( tags = ["manual"], ) + download_alternative_guestos_proposal( + name = "alternative_guestos_proposal.cbor", + tags = ["manual"], + ) + # -------------------- Assemble disk partitions --------------- # Build a list of custom partitions to allow "injecting" variant-specific partition logic. diff --git a/ic-os/guestos/envs/sev-recovery/BUILD.bazel b/ic-os/guestos/envs/sev-recovery/BUILD.bazel new file mode 100644 index 000000000000..347a46c52f5b --- /dev/null +++ b/ic-os/guestos/envs/sev-recovery/BUILD.bazel @@ -0,0 +1,24 @@ +load("//bazel:defs.bzl", "file_size_check") +load("//ic-os:defs.bzl", "icos_build") +load("//ic-os/guestos:defs.bzl", "image_deps") +load("//publish:defs.bzl", "artifact_bundle") + +# Builds an SEV recovery image when an elected proposal is available. +# The build requires the env variables ALTERNATIVE_GUESTOS_BASE_VERSION and ALTERNATIVE_GUESTOS_PROPOSAL_ID +# The base version is the commit id running on the node that we we are recovering. The proposal id is the accepted +# NNS proposal of type BlessAlternativeGuestOsVersion. It's important that the proposal corresponds to the base +# version, i.e the launch measurements match. This is checked when building the image. +# +# For example: +# +# ALTERNATIVE_GUESTOS_BASE_VERSION=62e841d27ea12451f504e74c54843b7cbf93ca4d ALTERNATIVE_GUESTOS_PROPOSAL_ID=123 \ +# bazel build //ic-os/guestos/envs/sev-recovery:update-img.tar.zst +icos_images = icos_build( + name = "sev-recovery", + build_alternative_guestos_image = True, + image_deps_func = image_deps, + mode = "prod", + visibility = [ + "//rs:ic-os-pkg", + ], +) \ No newline at end of file diff --git a/rs/ic_os/alternative_guestos/BUILD.bazel b/rs/ic_os/alternative_guestos/BUILD.bazel new file mode 100644 index 000000000000..82028b170031 --- /dev/null +++ b/rs/ic_os/alternative_guestos/BUILD.bazel @@ -0,0 +1,18 @@ +load("@rules_rust//rust:defs.bzl", "rust_library") + +package(default_visibility = ["//visibility:public"]) + +rust_library( + name = "alternative_guestos", + srcs = glob(["src/**/*.rs"]), + crate_name = "alternative_guestos", + deps = [ + "//rs/nns/constants", + "//rs/nns/governance/api", + "@crate_index//:anyhow", + "@crate_index//:candid", + "@crate_index//:ic-certificate-verification", + "@crate_index//:ic-certification", + "@crate_index//:serde_cbor", + ], +) diff --git a/rs/ic_os/alternative_guestos/Cargo.toml b/rs/ic_os/alternative_guestos/Cargo.toml new file mode 100644 index 000000000000..92b993187812 --- /dev/null +++ b/rs/ic_os/alternative_guestos/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "alternative_guestos" +edition.workspace = true + +[dependencies] +anyhow = { workspace = true } +candid = { workspace = true } +ic-certificate-verification = { workspace = true } +ic-certification = { workspace = true } +serde_cbor = { workspace = true } + +ic-nns-constants = { path = "../../nns/constants" } +ic-nns-governance-api = { path = "../../nns/governance/api" } diff --git a/rs/ic_os/alternative_guestos/src/lib.rs b/rs/ic_os/alternative_guestos/src/lib.rs new file mode 100644 index 000000000000..0f69202813a3 --- /dev/null +++ b/rs/ic_os/alternative_guestos/src/lib.rs @@ -0,0 +1 @@ +pub mod proposal; diff --git a/rs/ic_os/open_rootfs/src/proposal.rs b/rs/ic_os/alternative_guestos/src/proposal.rs similarity index 94% rename from rs/ic_os/open_rootfs/src/proposal.rs rename to rs/ic_os/alternative_guestos/src/proposal.rs index 72c40b60d8f6..a4978fb208bf 100644 --- a/rs/ic_os/open_rootfs/src/proposal.rs +++ b/rs/ic_os/alternative_guestos/src/proposal.rs @@ -23,7 +23,7 @@ const NNS_PUBLIC_KEY: &[u8; 133] = b"\x30\x81\x82\x30\x1d\x06\x0d\x2b\x06\x01\x0 /// (e.g. by using ic-agent's Agent::update().call().and_wait()) and storing it in a file. pub fn read_and_verify_signed_bless_alternative_guest_os_version_proposal( proposal_path: &Path, - #[cfg(any(feature = "dev", test))] nns_public_key_override: Option<&[u8]>, + nns_public_key_override: Option<&[u8]>, ) -> Result { ensure!( proposal_path.exists(), @@ -37,10 +37,7 @@ pub fn read_and_verify_signed_bless_alternative_guest_os_version_proposal( let certificate: Certificate = serde_cbor::from_slice(&proposal_bytes) .context("Failed to deserialize Certificate from CBOR")?; - let nns_public_key = NNS_PUBLIC_KEY; - // Allow overriding the NNS public key if running in dev or test mode - #[cfg(any(feature = "dev", test))] - let nns_public_key = nns_public_key_override.unwrap_or(nns_public_key); + let nns_public_key = nns_public_key_override.unwrap_or(NNS_PUBLIC_KEY); // Verify the certificate against the NNS public key. This is the proof that the proposal // came from the NNS governance canister. diff --git a/rs/ic_os/build_tools/alternative_guestos/BUILD.bazel b/rs/ic_os/build_tools/alternative_guestos/BUILD.bazel new file mode 100644 index 000000000000..e16cb3b561e7 --- /dev/null +++ b/rs/ic_os/build_tools/alternative_guestos/BUILD.bazel @@ -0,0 +1,28 @@ +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_test") + +package(default_visibility = ["//visibility:public"]) + +rust_binary( + name = "alternative_guestos", + srcs = glob(["src/**/*.rs"]), + deps = [ + "//rs/ic_os/alternative_guestos", + "//rs/nns/constants", + "//rs/nns/governance/api", + "//rs/nns/governance/conversions", + "//rs/protobuf", + "@crate_index//:anyhow", + "@crate_index//:candid", + "@crate_index//:clap", + "@crate_index//:ic-agent", + "@crate_index//:ic-certification", + "@crate_index//:serde_cbor", + "@crate_index//:serde_json", + "@crate_index//:tokio", + ], +) + +rust_test( + name = "alternative_guestos_test", + crate = ":alternative_guestos", +) \ No newline at end of file diff --git a/rs/ic_os/build_tools/alternative_guestos/Cargo.toml b/rs/ic_os/build_tools/alternative_guestos/Cargo.toml new file mode 100644 index 000000000000..83aa01ccb3d6 --- /dev/null +++ b/rs/ic_os/build_tools/alternative_guestos/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "alternative_guestos_tool" +edition.workspace = true + +[dependencies] +anyhow = { workspace = true } +candid = { workspace = true } +clap = { workspace = true } +ic-agent = { workspace = true } +ic-certification = { workspace = true } +serde_cbor = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } + +alternative_guestos = { path = "../../alternative_guestos" } +ic-nns-constants = { path = "../../../nns/constants" } +ic-nns-governance-api = { path = "../../../nns/governance/api" } +ic-nns-governance-conversions = { path = "../../../nns/governance/conversions" } +ic-protobuf = { path = "../../../protobuf" } \ No newline at end of file diff --git a/rs/ic_os/build_tools/alternative_guestos/src/download.rs b/rs/ic_os/build_tools/alternative_guestos/src/download.rs new file mode 100644 index 000000000000..76bb70d09e01 --- /dev/null +++ b/rs/ic_os/build_tools/alternative_guestos/src/download.rs @@ -0,0 +1,50 @@ +use anyhow::{Context, Result}; +use candid::{Encode, Principal}; +use ic_agent::Agent; +use alternative_guestos::proposal::read_and_verify_signed_bless_alternative_guest_os_version_proposal; +use ic_certification::Certificate; +use ic_nns_constants::GOVERNANCE_CANISTER_ID; +use std::fs; +use std::path::Path; + +pub async fn download_signed_proposal( + proposal_id: u64, + nns_url: &str, + output: &Path, +) -> Result<()> { + let governance_canister_id = Principal::from(GOVERNANCE_CANISTER_ID); + let agent = Agent::builder() + .with_url(nns_url) + .build() + .context("Failed to build NNS agent")?; + let args = Encode!(&proposal_id).context("Failed to encode proposal id")?; + let (_reply, certificate): (Vec, Certificate) = agent + .update(&governance_canister_id, "get_proposal_info") + .with_arg(args) + .call() + .and_wait() + .await + .context("Failed to download certified get_proposal_info response")?; + + fs::write( + output, + serde_cbor::to_vec(&certificate).context("Failed to serialize certificate as CBOR")?, + ) + .with_context(|| { + format!( + "Failed to write proposal certificate to {}", + output.display() + ) + })?; + + read_and_verify_signed_bless_alternative_guest_os_version_proposal(output, None).with_context( + || { + format!( + "Downloaded proposal at {} failed verification", + output.display() + ) + }, + )?; + + Ok(()) +} diff --git a/rs/ic_os/build_tools/alternative_guestos/src/main.rs b/rs/ic_os/build_tools/alternative_guestos/src/main.rs new file mode 100644 index 000000000000..ead22181a889 --- /dev/null +++ b/rs/ic_os/build_tools/alternative_guestos/src/main.rs @@ -0,0 +1,62 @@ +use crate::download::download_signed_proposal; +use crate::proposal_build::validate_measurements; +use anyhow::{Context, Result}; +use clap::{Parser, Subcommand}; +use alternative_guestos::proposal::read_and_verify_signed_bless_alternative_guest_os_version_proposal; +use std::fs; +use std::path::PathBuf; + +mod download; +mod proposal_build; + +#[derive(Parser)] +struct Args { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand)] +enum Command { + DownloadSignedProposal { + #[arg(long)] + proposal_id: u64, + #[arg(long, default_value = "https://ic0.app")] + nns_url: String, + #[arg(long)] + output: PathBuf, + }, + ValidateMeasurements { + #[arg(long)] + proposal_path: PathBuf, + #[arg(long)] + local_measurements_path: PathBuf, + }, +} + +#[tokio::main] +async fn main() -> Result<()> { + match Args::parse().command { + Command::DownloadSignedProposal { + proposal_id, + nns_url, + output, + } => download_signed_proposal(proposal_id, &nns_url, &output).await, + Command::ValidateMeasurements { + proposal_path, + local_measurements_path, + } => { + let proposal = read_and_verify_signed_bless_alternative_guest_os_version_proposal( + &proposal_path, + None, + )?; + let local_measurements = + fs::read_to_string(&local_measurements_path).with_context(|| { + format!( + "Failed to read local measurements from {}", + local_measurements_path.display() + ) + })?; + validate_measurements(&proposal, &local_measurements) + } + } +} diff --git a/rs/ic_os/build_tools/alternative_guestos/src/proposal_build.rs b/rs/ic_os/build_tools/alternative_guestos/src/proposal_build.rs new file mode 100644 index 000000000000..ce2b545c62f0 --- /dev/null +++ b/rs/ic_os/build_tools/alternative_guestos/src/proposal_build.rs @@ -0,0 +1,129 @@ +use anyhow::{Context, Result, ensure}; +use ic_nns_governance_api::BlessAlternativeGuestOsVersion; +use ic_nns_governance_conversions::convert_guest_launch_measurements_from_api_to_pb; +use ic_protobuf::registry::replica_version::v1::{GuestLaunchMeasurement, GuestLaunchMeasurements}; +use std::collections::HashSet; + +pub fn validate_measurements( + proposal: &BlessAlternativeGuestOsVersion, + local_measurements_json: &str, +) -> Result<()> { + let local_measurements: GuestLaunchMeasurements = serde_json::from_str(local_measurements_json) + .context("Failed to parse locally generated launch measurements JSON")?; + let proposal_measurements = proposal_measurements(proposal)?; + let local_measurements = local_measurements.guest_launch_measurements; + let local_measurement_bytes = measurement_bytes(&local_measurements); + let proposal_measurement_bytes = measurement_bytes(&proposal_measurements); + + let shared_measurement_count = local_measurement_bytes + .intersection(&proposal_measurement_bytes) + .count(); + + ensure!( + shared_measurement_count > 0, + "Local and proposal measurements do not overlap.\nLocal measurements: {:?}\nProposal measurements: {:?}", + local_measurements, + proposal_measurements, + ); + + Ok(()) +} + +fn measurement_bytes(measurements: &[GuestLaunchMeasurement]) -> HashSet> { + measurements + .iter() + .map(|measurement| measurement.measurement.clone()) + .collect() +} + +fn proposal_measurements( + proposal: &BlessAlternativeGuestOsVersion, +) -> Result> { + Ok(convert_guest_launch_measurements_from_api_to_pb( + proposal + .base_guest_launch_measurements + .clone() + .context("Proposal is missing base_guest_launch_measurements")?, + ) + .guest_launch_measurements) +} + +#[cfg(test)] +mod tests { + use super::*; + use ic_nns_governance_api::{ + GuestLaunchMeasurement as ApiGuestLaunchMeasurement, + GuestLaunchMeasurementMetadata as ApiGuestLaunchMeasurementMetadata, + GuestLaunchMeasurements as ApiGuestLaunchMeasurements, + }; + use serde_json::json; + + fn proposal( + kernel_cmdlines: &[&str], + measurements: &[Vec], + ) -> BlessAlternativeGuestOsVersion { + BlessAlternativeGuestOsVersion { + chip_ids: Some(vec![vec![0; 64]]), + rootfs_hash: Some("ignored-by-build-tool".into()), + base_guest_launch_measurements: Some(ApiGuestLaunchMeasurements { + guest_launch_measurements: Some( + kernel_cmdlines + .iter() + .zip(measurements.iter()) + .map(|(kernel_cmdline, measurement)| ApiGuestLaunchMeasurement { + measurement: Some(measurement.clone()), + metadata: Some(ApiGuestLaunchMeasurementMetadata { + kernel_cmdline: Some((*kernel_cmdline).into()), + vcpu_type: Some("EPYC-Genoa".into()), + }), + }) + .collect(), + ), + }), + } + } + + #[test] + fn accepts_exact_measurement_match() { + let proposal = proposal( + &[ + "console=ttyS0 root_hash=abcd", + "console=ttyS0 root_hash=abcd dfinity.tee=1", + ], + &[vec![1, 2], vec![3, 4]], + ); + let local_measurements = json!({ + "guest_launch_measurements": [ + {"measurement": [1, 2], "metadata": {"kernel_cmdline": "console=ttyS0 root_hash=abcd", "vcpu_type": "EPYC-Genoa"}}, + {"measurement": [3, 4], "metadata": {"kernel_cmdline": "console=ttyS0 root_hash=abcd dfinity.tee=1", "vcpu_type": "EPYC-Genoa"}} + ] + }); + + validate_measurements(&proposal, &local_measurements.to_string()).unwrap(); + } + + #[test] + fn accepts_when_local_has_extra_measurement_but_overlap_exists() { + let proposal = proposal(&["console=ttyS0 root_hash=abcd"], &[vec![1, 2]]); + let local_measurements = json!({ + "guest_launch_measurements": [ + {"measurement": [1, 2], "metadata": {"kernel_cmdline": "console=ttyS0 root_hash=abcd", "vcpu_type": "EPYC-Genoa"}}, + {"measurement": [3, 4], "metadata": {"kernel_cmdline": "console=ttyS0 root_hash=abcd dfinity.tee=1", "vcpu_type": "EPYC-Genoa"}} + ] + }); + + validate_measurements(&proposal, &local_measurements.to_string()).unwrap(); + } + + #[test] + fn rejects_when_measurements_do_not_overlap() { + let proposal = proposal(&["console=ttyS0 root_hash=abcd"], &[vec![1, 2]]); + let local_measurements = json!({ + "guest_launch_measurements": [ + {"measurement": [3, 4], "metadata": {"kernel_cmdline": "console=ttyS0 root_hash=other", "vcpu_type": "EPYC-Genoa"}} + ] + }); + + assert!(validate_measurements(&proposal, &local_measurements.to_string()).is_err()); + } +} diff --git a/rs/ic_os/config/tool/src/guestos/bootstrap_ic_node.rs b/rs/ic_os/config/tool/src/guestos/bootstrap_ic_node.rs index b5ac4d7ecd07..f4b5d0caa063 100644 --- a/rs/ic_os/config/tool/src/guestos/bootstrap_ic_node.rs +++ b/rs/ic_os/config/tool/src/guestos/bootstrap_ic_node.rs @@ -1,5 +1,4 @@ use anyhow::{Context, Result}; -#[cfg(target_os = "linux")] use config_types::GuestOSConfig; use fs_extra; use std::fs::{self, File}; diff --git a/rs/ic_os/open_rootfs/BUILD.bazel b/rs/ic_os/open_rootfs/BUILD.bazel index ecac7acd4624..d0d4fe9673a7 100644 --- a/rs/ic_os/open_rootfs/BUILD.bazel +++ b/rs/ic_os/open_rootfs/BUILD.bazel @@ -1,4 +1,4 @@ -load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_test") +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test") package(default_visibility = ["//visibility:public"]) @@ -7,6 +7,7 @@ rust_binary( srcs = glob(["src/**/*.rs"]), deps = [ # Keep sorted. + "//rs/ic_os/alternative_guestos", "//rs/ic_os/command_runner", "//rs/ic_os/config/tool:config_lib", "//rs/ic_os/config/types:config_types", @@ -37,6 +38,7 @@ rust_binary( crate_features = ["dev"], deps = [ # Keep sorted. + "//rs/ic_os/alternative_guestos", "//rs/ic_os/command_runner", "//rs/ic_os/config/tool:config_lib", "//rs/ic_os/config/types:config_types", @@ -75,3 +77,4 @@ rust_test( "@crate_index//:tempfile", ], ) + diff --git a/rs/ic_os/open_rootfs/Cargo.toml b/rs/ic_os/open_rootfs/Cargo.toml index 978f8eac468d..be896ea89fb5 100644 --- a/rs/ic_os/open_rootfs/Cargo.toml +++ b/rs/ic_os/open_rootfs/Cargo.toml @@ -21,6 +21,7 @@ serde_cbor = { workspace = true } tracing = { workspace = true } uuid = { workspace = true } +alternative_guestos = { path = "../alternative_guestos" } attestation = { path = "../sev/attestation" } command_runner = { path = "../command_runner" } config_tool = { path = "../config/tool" } diff --git a/rs/ic_os/open_rootfs/src/main.rs b/rs/ic_os/open_rootfs/src/main.rs index 8a098c700615..ced7b87103f4 100644 --- a/rs/ic_os/open_rootfs/src/main.rs +++ b/rs/ic_os/open_rootfs/src/main.rs @@ -1,5 +1,4 @@ mod partitions; -mod proposal; mod recovery; mod verity; diff --git a/rs/ic_os/open_rootfs/src/recovery.rs b/rs/ic_os/open_rootfs/src/recovery.rs index a51ef990bee8..2befc3f06152 100644 --- a/rs/ic_os/open_rootfs/src/recovery.rs +++ b/rs/ic_os/open_rootfs/src/recovery.rs @@ -1,10 +1,10 @@ use crate::partitions::get_boot_partition_uuid; -use crate::proposal::read_and_verify_signed_bless_alternative_guest_os_version_proposal; use anyhow::{Context, Result}; use attestation::attestation_package::AttestationPackageVerifier; use attestation::custom_data::{SevCustomData, SevCustomDataNamespace}; use command_runner::CommandRunner; use config_types::GuestOSConfig; +use alternative_guestos::proposal::read_and_verify_signed_bless_alternative_guest_os_version_proposal; use ic_device::mount::{ FileSystem, MountOptions, MountedPartition, PartitionProvider, PartitionSelector, }; @@ -39,6 +39,8 @@ pub fn extract_and_verify_recovery_rootfs_hash( #[cfg(any(feature = "dev", test))] let nns_public_key_override = get_nns_public_key_override(config_mount.mount_point())?; + #[cfg(not(any(feature = "dev", test)))] + let nns_public_key_override: Option> = None; drop(config_mount); @@ -52,7 +54,6 @@ pub fn extract_and_verify_recovery_rootfs_hash( let proposal = read_and_verify_signed_bless_alternative_guest_os_version_proposal( &boot_mount.mount_point().join(RECOVERY_PROPOSAL_FILE_NAME), - #[cfg(any(feature = "dev", test))] nns_public_key_override.as_deref(), )?; From 37e2120f7b7f75201b178d4ebe6130239208279f Mon Sep 17 00:00:00 2001 From: IDX GitHub Automation Date: Thu, 25 Jun 2026 23:35:41 +0000 Subject: [PATCH 2/6] Automatically updated Cargo*.lock --- Cargo.lock | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f81308557a5d..c34c42d0c813 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -382,10 +382,24 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "alternative_guestos" +version = "0.0.0" +dependencies = [ + "anyhow", + "candid", + "ic-certificate-verification", + "ic-certification 3.1.0", + "ic-nns-constants", + "ic-nns-governance-api", + "serde_cbor", +] + [[package]] name = "alternative_guestos_tool" version = "0.0.0" dependencies = [ + "alternative_guestos", "anyhow", "candid", "clap 4.6.0", @@ -395,7 +409,6 @@ dependencies = [ "ic-nns-governance-api", "ic-nns-governance-conversions", "ic-protobuf", - "ic_os_alternative_guestos", "serde_cbor", "serde_json", "tokio", @@ -16323,19 +16336,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "ic_os_alternative_guestos" -version = "0.0.0" -dependencies = [ - "anyhow", - "candid", - "ic-certificate-verification", - "ic-certification 3.1.0", - "ic-nns-constants", - "ic-nns-governance-api", - "serde_cbor", -] - [[package]] name = "ic_os_logging" version = "0.0.0" @@ -19160,6 +19160,7 @@ dependencies = [ name = "open_rootfs" version = "0.0.0" dependencies = [ + "alternative_guestos", "anyhow", "attestation", "candid", @@ -19176,7 +19177,6 @@ dependencies = [ "ic-nns-governance-api", "ic-types", "ic_device", - "ic_os_alternative_guestos", "ic_os_logging", "linux_kernel_command_line", "pem", From 9e13732943981a71969fd653d57169529e6af388 Mon Sep 17 00:00:00 2001 From: IDX GitHub Automation Date: Thu, 25 Jun 2026 23:41:03 +0000 Subject: [PATCH 3/6] Automatically fixing code for linting and formatting issues --- ic-os/alternative_guestos.bzl | 1 - ic-os/defs.bzl | 10 +++++----- ic-os/guestos/envs/sev-recovery/BUILD.bazel | 4 +--- rs/ic_os/build_tools/alternative_guestos/BUILD.bazel | 2 +- .../build_tools/alternative_guestos/src/download.rs | 2 +- rs/ic_os/build_tools/alternative_guestos/src/main.rs | 2 +- rs/ic_os/open_rootfs/BUILD.bazel | 3 +-- rs/ic_os/open_rootfs/src/recovery.rs | 2 +- 8 files changed, 11 insertions(+), 15 deletions(-) diff --git a/ic-os/alternative_guestos.bzl b/ic-os/alternative_guestos.bzl index 93f19fac3acf..b8b8fc1bd1a8 100644 --- a/ic-os/alternative_guestos.bzl +++ b/ic-os/alternative_guestos.bzl @@ -94,4 +94,3 @@ tar --create --file "$@" --numeric-owner -C "$$tmpdir/bootfs" . target_compatible_with = target_compatible_with, tools = ["//:fuse2fs"], ) - diff --git a/ic-os/defs.bzl b/ic-os/defs.bzl index f990c9236900..14ea5a37b590 100644 --- a/ic-os/defs.bzl +++ b/ic-os/defs.bzl @@ -247,11 +247,11 @@ def icos_build( extra_files = { k: v for k, v in ( - image_deps["bootfs"].items() + [ - (version_txt, "/version.txt:0644"), - (boot_args, "/boot_args:0644"), - (image_deps["grub_config"], "/grub.cfg:0644"), - ] + image_deps["bootfs"].items() + [ + (version_txt, "/version.txt:0644"), + (boot_args, "/boot_args:0644"), + (image_deps["grub_config"], "/grub.cfg:0644"), + ] ) }, tags = ["manual", "no-cache"], diff --git a/ic-os/guestos/envs/sev-recovery/BUILD.bazel b/ic-os/guestos/envs/sev-recovery/BUILD.bazel index 347a46c52f5b..c36019ffff8a 100644 --- a/ic-os/guestos/envs/sev-recovery/BUILD.bazel +++ b/ic-os/guestos/envs/sev-recovery/BUILD.bazel @@ -1,7 +1,5 @@ -load("//bazel:defs.bzl", "file_size_check") load("//ic-os:defs.bzl", "icos_build") load("//ic-os/guestos:defs.bzl", "image_deps") -load("//publish:defs.bzl", "artifact_bundle") # Builds an SEV recovery image when an elected proposal is available. # The build requires the env variables ALTERNATIVE_GUESTOS_BASE_VERSION and ALTERNATIVE_GUESTOS_PROPOSAL_ID @@ -21,4 +19,4 @@ icos_images = icos_build( visibility = [ "//rs:ic-os-pkg", ], -) \ No newline at end of file +) diff --git a/rs/ic_os/build_tools/alternative_guestos/BUILD.bazel b/rs/ic_os/build_tools/alternative_guestos/BUILD.bazel index e16cb3b561e7..28685b5606c2 100644 --- a/rs/ic_os/build_tools/alternative_guestos/BUILD.bazel +++ b/rs/ic_os/build_tools/alternative_guestos/BUILD.bazel @@ -25,4 +25,4 @@ rust_binary( rust_test( name = "alternative_guestos_test", crate = ":alternative_guestos", -) \ No newline at end of file +) diff --git a/rs/ic_os/build_tools/alternative_guestos/src/download.rs b/rs/ic_os/build_tools/alternative_guestos/src/download.rs index 76bb70d09e01..fdd7159504b4 100644 --- a/rs/ic_os/build_tools/alternative_guestos/src/download.rs +++ b/rs/ic_os/build_tools/alternative_guestos/src/download.rs @@ -1,7 +1,7 @@ +use alternative_guestos::proposal::read_and_verify_signed_bless_alternative_guest_os_version_proposal; use anyhow::{Context, Result}; use candid::{Encode, Principal}; use ic_agent::Agent; -use alternative_guestos::proposal::read_and_verify_signed_bless_alternative_guest_os_version_proposal; use ic_certification::Certificate; use ic_nns_constants::GOVERNANCE_CANISTER_ID; use std::fs; diff --git a/rs/ic_os/build_tools/alternative_guestos/src/main.rs b/rs/ic_os/build_tools/alternative_guestos/src/main.rs index ead22181a889..ecb78bd3e0d9 100644 --- a/rs/ic_os/build_tools/alternative_guestos/src/main.rs +++ b/rs/ic_os/build_tools/alternative_guestos/src/main.rs @@ -1,8 +1,8 @@ use crate::download::download_signed_proposal; use crate::proposal_build::validate_measurements; +use alternative_guestos::proposal::read_and_verify_signed_bless_alternative_guest_os_version_proposal; use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; -use alternative_guestos::proposal::read_and_verify_signed_bless_alternative_guest_os_version_proposal; use std::fs; use std::path::PathBuf; diff --git a/rs/ic_os/open_rootfs/BUILD.bazel b/rs/ic_os/open_rootfs/BUILD.bazel index d0d4fe9673a7..dd60ff7a80b9 100644 --- a/rs/ic_os/open_rootfs/BUILD.bazel +++ b/rs/ic_os/open_rootfs/BUILD.bazel @@ -1,4 +1,4 @@ -load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test") +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_test") package(default_visibility = ["//visibility:public"]) @@ -77,4 +77,3 @@ rust_test( "@crate_index//:tempfile", ], ) - diff --git a/rs/ic_os/open_rootfs/src/recovery.rs b/rs/ic_os/open_rootfs/src/recovery.rs index 2befc3f06152..a5e32bbe5a2e 100644 --- a/rs/ic_os/open_rootfs/src/recovery.rs +++ b/rs/ic_os/open_rootfs/src/recovery.rs @@ -1,10 +1,10 @@ use crate::partitions::get_boot_partition_uuid; +use alternative_guestos::proposal::read_and_verify_signed_bless_alternative_guest_os_version_proposal; use anyhow::{Context, Result}; use attestation::attestation_package::AttestationPackageVerifier; use attestation::custom_data::{SevCustomData, SevCustomDataNamespace}; use command_runner::CommandRunner; use config_types::GuestOSConfig; -use alternative_guestos::proposal::read_and_verify_signed_bless_alternative_guest_os_version_proposal; use ic_device::mount::{ FileSystem, MountOptions, MountedPartition, PartitionProvider, PartitionSelector, }; From 7bdff20103cce95f258a40bfcfd67f49e32c6b94 Mon Sep 17 00:00:00 2001 From: David Frank Date: Sun, 28 Jun 2026 19:31:47 +0200 Subject: [PATCH 4/6] Move extract_boot_partition_files into the test loop --- ic-os/defs.bzl | 84 ++++++++++++++++++++++++++------------------------ 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/ic-os/defs.bzl b/ic-os/defs.bzl index 14ea5a37b590..36e14b9f3667 100644 --- a/ic-os/defs.bzl +++ b/ic-os/defs.bzl @@ -143,38 +143,6 @@ def icos_build( target_compatible_with = ["@platforms//os:linux"], ) - # To be 100% correct, this rule should also live below and be duplicated to the default and the test variant but - # since the extracted files are the same in both variants, it's a bit simpler to not duplicate it and only extract - # from the default variant. - native.genrule( - name = "extract_boot_partition_files", - srcs = [":partition-boot.tzst"], - outs = [ - "extracted_boot_args", - "extracted_initrd.img", - "extracted_vmlinuz", - "extracted_OVMF_SEV.fd", - ], - cmd = """ - tmpdir="$$(mktemp -d)" - trap 'rm -rf "$$tmpdir"' EXIT - - tar --extract -a --file "$<" --directory "$$tmpdir" - - # initrd and vmlinuz are symlinks, first extract the symlink targets - initrd_target="$$(/usr/sbin/debugfs -R "stat /initrd.img" "$$tmpdir/partition.img" | sed -n 's/^Fast link dest: "\\(.*\\)"$$/\\1/p')" - vmlinuz_target="$$(/usr/sbin/debugfs -R "stat /vmlinuz" "$$tmpdir/partition.img" | sed -n 's/^Fast link dest: "\\(.*\\)"$$/\\1/p')" - - /usr/sbin/debugfs -R "dump /boot_args $(@D)/extracted_boot_args" "$$tmpdir/partition.img" - /usr/sbin/debugfs -R "dump /OVMF_SEV.fd $(@D)/extracted_OVMF_SEV.fd" "$$tmpdir/partition.img" - /usr/sbin/debugfs -R "dump /$$initrd_target $(@D)/extracted_initrd.img" "$$tmpdir/partition.img" - /usr/sbin/debugfs -R "dump /$$vmlinuz_target $(@D)/extracted_vmlinuz" "$$tmpdir/partition.img" - """, - message = "Extracting files from boot partition", - tags = ["manual"], - target_compatible_with = ["@platforms//os:linux"], - ) - # -------------------- Extract root and boot partitions -------------------- # NOTE: e2fsdroid does not support filenames with spaces, fortunately, @@ -257,6 +225,42 @@ def icos_build( tags = ["manual", "no-cache"], ) + # Extract individual files (boot args, initrd, vmlinuz, OVMF firmware) from the + # boot partition image for use in launch measurement generation and boot arg aliasing. + extracted_boot_args = "extracted_boot" + test_suffix + "_args" + extracted_initrd = "extracted_initrd" + test_suffix + ".img" + extracted_vmlinuz = "extracted_vmlinuz" + test_suffix + extracted_ovmf_sev = "extracted_OVMF_SEV" + test_suffix + ".fd" + + native.genrule( + name = "extract_boot_partition_files" + test_suffix, + srcs = [":" + partition_boot_tzst], + outs = [ + extracted_boot_args, + extracted_initrd, + extracted_vmlinuz, + extracted_ovmf_sev, + ], + cmd = """ + tmpdir="$$(mktemp -d)" + trap 'rm -rf "$$tmpdir"' EXIT + + tar --extract -a --file "$<" --directory "$$tmpdir" + + # initrd and vmlinuz are symlinks, first extract the symlink targets + initrd_target="$$(/usr/sbin/debugfs -R "stat /initrd.img" "$$tmpdir/partition.img" | sed -n 's/^Fast link dest: "\\(.*\\)"$$/\\1/p')" + vmlinuz_target="$$(/usr/sbin/debugfs -R "stat /vmlinuz" "$$tmpdir/partition.img" | sed -n 's/^Fast link dest: "\\(.*\\)"$$/\\1/p')" + + /usr/sbin/debugfs -R "dump /boot_args $(@D)/""" + extracted_boot_args + """" "$$tmpdir/partition.img" + /usr/sbin/debugfs -R "dump /OVMF_SEV.fd $(@D)/""" + extracted_ovmf_sev + """" "$$tmpdir/partition.img" + /usr/sbin/debugfs -R "dump /$$initrd_target $(@D)/""" + extracted_initrd + """" "$$tmpdir/partition.img" + /usr/sbin/debugfs -R "dump /$$vmlinuz_target $(@D)/""" + extracted_vmlinuz + """" "$$tmpdir/partition.img" + """, + message = "Extracting files from boot partition", + tags = ["manual"], + target_compatible_with = ["@platforms//os:linux"], + ) + # The kernel command line (boot args) is usually generated from boot_args_template: # - For OS requiring root signing: Template includes ROOT_HASH placeholder that gets substituted with dm-verity hash # - For OS not requiring root signing: Template is used as-is without ROOT_HASH substitution @@ -298,7 +302,7 @@ def icos_build( if build_alternative_guestos_image: native.alias( name = boot_args, - actual = ":extracted_boot_args", + actual = ":" + extracted_boot_args, tags = ["manual"], ) else: @@ -315,7 +319,7 @@ def icos_build( native.alias(name = partition_root_signed_tzst, actual = partition_root_unsigned_tzst, tags = ["manual", "no-cache"]) native.alias( name = boot_args, - actual = ":extracted_boot_args" if build_alternative_guestos_image else ":boot_args_template", + actual = ":" + extracted_boot_args if build_alternative_guestos_image else ":boot_args_template", tags = ["manual"], ) @@ -336,21 +340,21 @@ def icos_build( # are validated against the proposal *in the same genrule* so that the # validation can never become a leaf target that is silently skipped. measurement_srcs = [ - ":extracted_OVMF_SEV.fd", - ":extracted_boot_args", - ":extracted_initrd.img", - ":extracted_vmlinuz", + ":" + extracted_ovmf_sev, + ":" + extracted_boot_args, + ":" + extracted_initrd, + ":" + extracted_vmlinuz, ] measurement_tools = ["//ic-os:sev-snp-measure"] measurement_cmd = r""" - source $(execpath :extracted_boot_args) + source $(execpath :""" + extracted_boot_args + r""") # Create GuestLaunchMeasurements JSON for each CPU generation, vCPU count, and boot slot (for vcpu_type in """ + " ".join(vcpu_types) + """; do for vcpus in """ + vcpu_configs + """; do # Note: We only create launch measurements for the TEE boot arg variants # (BOOT_ARGS_TEE_A and BOOT_ARGS_TEE_B) for cmdline in "$$BOOT_ARGS_TEE_A" "$$BOOT_ARGS_TEE_B"; do - hex=$$($(execpath //ic-os:sev-snp-measure) --mode snp --vcpus $$vcpus --ovmf "$(execpath extracted_OVMF_SEV.fd)" --vcpu-type "$$vcpu_type" --append "$$cmdline" --initrd "$(location extracted_initrd.img)" --kernel "$(location extracted_vmlinuz)") + hex=$$($(execpath //ic-os:sev-snp-measure) --mode snp --vcpus $$vcpus --ovmf "$(execpath """ + extracted_ovmf_sev + """)" --vcpu-type "$$vcpu_type" --append "$$cmdline" --initrd "$(location """ + extracted_initrd + """)" --kernel "$(location """ + extracted_vmlinuz + """)") # Convert hex string to decimal list, e.g. "abcd" -> 171\\n205 measurement=$$(echo -n "$$hex" | fold -w2 | sed "s/^/0x/" | xargs printf "%d\n") jq -na --arg cmd "$$cmdline" --arg m "$$measurement" --arg vcpu_type "$$vcpu_type" '{ From 3656d9531a876dd4ca4e1ad013b9b559a6e3d45f Mon Sep 17 00:00:00 2001 From: David Frank Date: Thu, 2 Jul 2026 15:23:32 +0200 Subject: [PATCH 5/6] Address comments --- ic-os/defs.bzl | 8 ++------ ic-os/guestos/envs/sev-recovery/BUILD.bazel | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/ic-os/defs.bzl b/ic-os/defs.bzl index 36e14b9f3667..b90374886392 100644 --- a/ic-os/defs.bzl +++ b/ic-os/defs.bzl @@ -273,19 +273,15 @@ def icos_build( if image_deps.get("requires_root_signing", False): # Sign the root partition and substitute ROOT_HASH in boot args - signed_partition_root_hash = partition_root_hash - if build_alternative_guestos_image: - signed_partition_root_hash = partition_root_hash + ".local" - native.genrule( name = "generate-" + partition_root_signed_tzst, testonly = malicious, srcs = [partition_root_unsigned_tzst], - outs = [partition_root_signed_tzst, signed_partition_root_hash], + outs = [partition_root_signed_tzst, partition_root_hash], cmd = "$(location //toolchains/sysimage:proc_wrapper) " + "$(location //toolchains/sysimage:verity_sign) " + "-i $< -o $(location :" + partition_root_signed_tzst + ") " + - "-r $(location " + signed_partition_root_hash + ") " + + "-r $(location " + partition_root_hash + ") " + "--dflate $(location //rs/ic_os/build_tools/dflate) " + "--zstd $(location @zstd//:zstd_cli)", executable = False, diff --git a/ic-os/guestos/envs/sev-recovery/BUILD.bazel b/ic-os/guestos/envs/sev-recovery/BUILD.bazel index c36019ffff8a..6a2b80f2f396 100644 --- a/ic-os/guestos/envs/sev-recovery/BUILD.bazel +++ b/ic-os/guestos/envs/sev-recovery/BUILD.bazel @@ -3,7 +3,7 @@ load("//ic-os/guestos:defs.bzl", "image_deps") # Builds an SEV recovery image when an elected proposal is available. # The build requires the env variables ALTERNATIVE_GUESTOS_BASE_VERSION and ALTERNATIVE_GUESTOS_PROPOSAL_ID -# The base version is the commit id running on the node that we we are recovering. The proposal id is the accepted +# The base version is the commit id running on the node that we are recovering. The proposal id is the accepted # NNS proposal of type BlessAlternativeGuestOsVersion. It's important that the proposal corresponds to the base # version, i.e the launch measurements match. This is checked when building the image. # From 63eb2d88846c1615391aa7a5fe6abde5bd066b66 Mon Sep 17 00:00:00 2001 From: David Frank Date: Tue, 14 Jul 2026 15:27:58 +0200 Subject: [PATCH 6/6] Move all downloads to build-sev-recovery.sh --- .gitignore | 5 + bazel/BUILD.bazel | 12 --- bazel/workspace_status.sh | 7 -- ic-os/alternative_guestos.bzl | 96 ------------------- ic-os/defs.bzl | 41 ++++++-- ic-os/guestos/envs/sev-recovery/BUILD.bazel | 22 +++-- .../envs/sev-recovery/build-sev-recovery.sh | 60 ++++++++++++ 7 files changed, 109 insertions(+), 134 deletions(-) delete mode 100644 ic-os/alternative_guestos.bzl create mode 100755 ic-os/guestos/envs/sev-recovery/build-sev-recovery.sh diff --git a/.gitignore b/.gitignore index 763b299b1cf2..bf515d0fdbf9 100644 --- a/.gitignore +++ b/.gitignore @@ -79,6 +79,11 @@ disk.img *-img.tar.zst dev-root-ca.crt +# Downloaded SEV recovery inputs (fetched by build-sev-recovery.sh) +ic-os/guestos/envs/sev-recovery/alternative_guestos_proposal.cbor +ic-os/guestos/envs/sev-recovery/base-update-img.tar.zst +ic-os/guestos/envs/sev-recovery/update-img-*.tar.zst + # IC-OS binaries cpp/**/infogetty cpp/**/infogetty.o diff --git a/bazel/BUILD.bazel b/bazel/BUILD.bazel index 002340e593cc..4f9c2fa18326 100644 --- a/bazel/BUILD.bazel +++ b/bazel/BUILD.bazel @@ -60,18 +60,6 @@ string_flag( visibility = ["//visibility:public"], ) -write_stable_status_file_var( - name = "alternative_guestos_proposal_id.txt", - varname = "STABLE_ALTERNATIVE_GUESTOS_PROPOSAL_ID", - visibility = ["//visibility:public"], -) - -write_stable_status_file_var( - name = "alternative_guestos_base_version.txt", - varname = "STABLE_ALTERNATIVE_GUESTOS_BASE_VERSION", - visibility = ["//visibility:public"], -) - write_stable_status_file_var( name = "version.txt", varname = "STABLE_VERSION", diff --git a/bazel/workspace_status.sh b/bazel/workspace_status.sh index 40ce47ed9230..00598c794bde 100755 --- a/bazel/workspace_status.sh +++ b/bazel/workspace_status.sh @@ -21,13 +21,6 @@ else exit 1 fi -if [ -n "${ALTERNATIVE_GUESTOS_PROPOSAL_ID:-}" ]; then - echo "STABLE_ALTERNATIVE_GUESTOS_PROPOSAL_ID ${ALTERNATIVE_GUESTOS_PROPOSAL_ID}" -fi -if [ -n "${ALTERNATIVE_GUESTOS_BASE_VERSION:-}" ]; then - echo "STABLE_ALTERNATIVE_GUESTOS_BASE_VERSION ${ALTERNATIVE_GUESTOS_BASE_VERSION}" -fi - # Used as farm metadata FARM_METADATA="USER=${USER:-${HOSTUSER:-$(whoami)}}" if [ -n "${CI_JOB_NAME:-}" ]; then diff --git a/ic-os/alternative_guestos.bzl b/ic-os/alternative_guestos.bzl deleted file mode 100644 index b8b8fc1bd1a8..000000000000 --- a/ic-os/alternative_guestos.bzl +++ /dev/null @@ -1,96 +0,0 @@ -def _download_alternative_guestos_proposal_impl(ctx): - output = ctx.actions.declare_file(ctx.label.name) - - command = """ -set -euo pipefail - -proposal_id="$(cat {proposal_id_file})" - -if [[ -z "$proposal_id" ]]; then - echo "//{package}:{name} requires ALTERNATIVE_GUESTOS_PROPOSAL_ID to be set in the environment before invoking Bazel." >&2 - exit 1 -fi - -{tool} download-signed-proposal \ - --proposal-id "$proposal_id" \ - --nns-url {nns_url} \ - --output {output} -""".format( - name = ctx.label.name, - nns_url = ctx.attr.nns_url, - output = output.path, - package = ctx.label.package, - proposal_id_file = ctx.file._proposal_id_file.path, - tool = ctx.executable._tool.path, - ) - - ctx.actions.run_shell( - command = command, - inputs = [ctx.file._proposal_id_file], - outputs = [output], - tools = [ctx.attr._tool.files_to_run], - mnemonic = "DownloadAlternativeGuestosProposal", - ) - - return [DefaultInfo(files = depset([output]))] - -download_alternative_guestos_proposal = rule( - implementation = _download_alternative_guestos_proposal_impl, - attrs = { - "nns_url": attr.string(default = "https://ic0.app"), - "_proposal_id_file": attr.label( - allow_single_file = True, - default = Label("//bazel:alternative_guestos_proposal_id.txt"), - ), - "_tool": attr.label( - default = Label("//rs/ic_os/build_tools/alternative_guestos"), - executable = True, - cfg = "exec", - ), - }, -) - -# Creates a tarball containing the bootfs file tree extracted from a released GuestOS. -def prepare_alternative_guestos_base_bootfs_tree_tar(name, out, tags = None, target_compatible_with = None): - native.genrule( - name = name, - srcs = ["//bazel:alternative_guestos_base_version.txt"], - outs = [out], - cmd = """ -set -euo pipefail - -base_version="$$(cat $<)" - -if [[ -z "$$base_version" ]]; then - echo "//{package}:{name} requires ALTERNATIVE_GUESTOS_BASE_VERSION to be set in the environment before invoking Bazel." >&2 - exit 1 -fi - -tmpdir="$$(mktemp -d)" -mounted=0 -cleanup() {{ - set +e - if [[ "$$mounted" -eq 1 ]]; then - fusermount3 -u "$$tmpdir/bootfs" || fusermount -u "$$tmpdir/bootfs" || umount "$$tmpdir/bootfs" - fi - rm -rf "$$tmpdir" -}} -trap cleanup EXIT - -curl --fail --silent --show-error --location \ - "https://download.dfinity.systems/ic/$$base_version/guest-os/update-img/update-img.tar.zst" \ - | tar --extract --zstd --to-stdout --file - boot.img > "$$tmpdir/boot.img" - -mkdir "$$tmpdir/bootfs" -$(location //:fuse2fs) -o ro,norecovery,fakeroot "$$tmpdir/boot.img" "$$tmpdir/bootfs" -mounted=1 -tar --create --file "$@" --numeric-owner -C "$$tmpdir/bootfs" . -""".format( - name = name, - package = native.package_name(), - ), - message = "Downloading alternative GuestOS base boot image and converting it to tar via fuse2fs", - tags = tags, - target_compatible_with = target_compatible_with, - tools = ["//:fuse2fs"], - ) diff --git a/ic-os/defs.bzl b/ic-os/defs.bzl index b90374886392..60d4ade94182 100644 --- a/ic-os/defs.bzl +++ b/ic-os/defs.bzl @@ -12,7 +12,6 @@ This macro defines the overall build process for ICOS images, including: load("@bazel_skylib//rules:copy_file.bzl", "copy_file") load("@rules_shell//shell:sh_binary.bzl", "sh_binary") load("//bazel:defs.bzl", "zstd_compress") -load("//ic-os:alternative_guestos.bzl", "download_alternative_guestos_proposal", "prepare_alternative_guestos_base_bootfs_tree_tar") load("//ic-os/bootloader:defs.bzl", "build_grub_partition") load("//ic-os/components:defs.bzl", "tree_hash") load("//ic-os/components/conformance_tests:defs.bzl", "component_file_references_test") @@ -136,11 +135,40 @@ def icos_build( tags = ["manual"], ) - prepare_alternative_guestos_base_bootfs_tree_tar( + # Extract the bootfs tree from the base GuestOS update image. The image is + # supplied by build-sev-recovery as a source file `base-update-img.tar.zst`, + # a symlink to a version-named `update-img-.tar.zst` + # (see //ic-os/guestos/envs/sev-recovery:build-sev-recovery). + native.genrule( name = "alternative_guestos_base_bootfs_tree_tar", - out = "alternative_guestos_base_bootfs_tree.tar", - tags = ["manual", "no-cache", "requires-network"], + srcs = [":base-update-img.tar.zst"], + outs = ["alternative_guestos_base_bootfs_tree.tar"], + cmd = """ +set -euo pipefail + +tmpdir=$$(mktemp -d) +mounted=0 +cleanup() { + set +e + if [[ $$mounted -eq 1 ]]; then + fusermount3 -u "$$tmpdir/bootfs" || fusermount -u "$$tmpdir/bootfs" || umount "$$tmpdir/bootfs" + fi + rm -rf "$$tmpdir" +} +trap cleanup EXIT + +# Extract boot.img from the base GuestOS update image. +tar --extract --zstd --to-stdout --file "$<" boot.img > "$$tmpdir/boot.img" + +mkdir "$$tmpdir/bootfs" +$(location //:fuse2fs) -o ro,norecovery,fakeroot "$$tmpdir/boot.img" "$$tmpdir/bootfs" +mounted=1 +tar --create --file "$@" --numeric-owner -C "$$tmpdir/bootfs" . +""", + message = "Extracting base GuestOS boot partition via fuse2fs", + tags = ["manual", "no-cache"], target_compatible_with = ["@platforms//os:linux"], + tools = ["//:fuse2fs"], ) # -------------------- Extract root and boot partitions -------------------- @@ -401,11 +429,6 @@ def icos_build( tags = ["manual"], ) - download_alternative_guestos_proposal( - name = "alternative_guestos_proposal.cbor", - tags = ["manual"], - ) - # -------------------- Assemble disk partitions --------------- # Build a list of custom partitions to allow "injecting" variant-specific partition logic. diff --git a/ic-os/guestos/envs/sev-recovery/BUILD.bazel b/ic-os/guestos/envs/sev-recovery/BUILD.bazel index 6a2b80f2f396..368e80bc6530 100644 --- a/ic-os/guestos/envs/sev-recovery/BUILD.bazel +++ b/ic-os/guestos/envs/sev-recovery/BUILD.bazel @@ -1,16 +1,10 @@ +load("@rules_shell//shell:sh_binary.bzl", "sh_binary") load("//ic-os:defs.bzl", "icos_build") load("//ic-os/guestos:defs.bzl", "image_deps") -# Builds an SEV recovery image when an elected proposal is available. -# The build requires the env variables ALTERNATIVE_GUESTOS_BASE_VERSION and ALTERNATIVE_GUESTOS_PROPOSAL_ID -# The base version is the commit id running on the node that we are recovering. The proposal id is the accepted -# NNS proposal of type BlessAlternativeGuestOsVersion. It's important that the proposal corresponds to the base -# version, i.e the launch measurements match. This is checked when building the image. -# -# For example: -# -# ALTERNATIVE_GUESTOS_BASE_VERSION=62e841d27ea12451f504e74c54843b7cbf93ca4d ALTERNATIVE_GUESTOS_PROPOSAL_ID=123 \ -# bazel build //ic-os/guestos/envs/sev-recovery:update-img.tar.zst +# Builds an SEV recovery image when an elected proposal is available: +# bazel run //ic-os/guestos/envs/sev-recovery:build-sev-recovery -- \ +# --base= --proposal= icos_images = icos_build( name = "sev-recovery", build_alternative_guestos_image = True, @@ -20,3 +14,11 @@ icos_images = icos_build( "//rs:ic-os-pkg", ], ) + +# Downloads the certified recovery proposal (the only non-hermetic step) and then +# builds the SEV recovery image. See the script header for details. +sh_binary( + name = "build-sev-recovery", + srcs = ["build-sev-recovery.sh"], + visibility = ["//visibility:public"], +) diff --git a/ic-os/guestos/envs/sev-recovery/build-sev-recovery.sh b/ic-os/guestos/envs/sev-recovery/build-sev-recovery.sh new file mode 100755 index 000000000000..3e531b643912 --- /dev/null +++ b/ic-os/guestos/envs/sev-recovery/build-sev-recovery.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# +# Builds an SEV recovery GuestOS image. +# +# Usage: +# bazel run //ic-os/guestos/envs/sev-recovery:build-sev-recovery -- \ +# --base= --proposal= +# +# --base git commit of the GuestOS release running on the node being +# recovered. The matching GuestOS update image is downloaded here. +# --proposal id of the accepted BlessAlternativeGuestOsVersion proposal that +# corresponds to the base version (its launch measurements must +# match, which is checked at build time). + +set -euo pipefail + +# `bazel run` executes this script from inside its output tree; switch back to +# the workspace so the nested `bazel` invocations and relative paths work. +WORKSPACE="${BUILD_WORKING_DIRECTORY:?must be invoked via \`bazel run\`}" +cd "$WORKSPACE" + +base_version="" +proposal_id="" +for arg in "$@"; do + case "$arg" in + --base=*) base_version="${arg#--base=}" ;; + --proposal=*) proposal_id="${arg#--proposal=}" ;; + *) echo "unknown argument: $arg" >&2; exit 1 ;; + esac +done + +if [[ -z "$base_version" || -z "$proposal_id" ]]; then + echo "usage: build-sev-recovery -- --base= --proposal=" >&2 + exit 1 +fi + +TARGET_PACKAGE="ic-os/guestos/envs/sev-recovery" +CBOR="$TARGET_PACKAGE/alternative_guestos_proposal.cbor" +IMG="$TARGET_PACKAGE/update-img-$base_version.tar.zst" +LINK="$TARGET_PACKAGE/base-update-img.tar.zst" +UPDATE_IMG_URL="https://download.dfinity.systems/ic/$base_version/guest-os/update-img/update-img.tar.zst" +TARGET="//$TARGET_PACKAGE:update-img.tar.zst" + +echo "==> Downloading base GuestOS update image ($base_version)..." +# Cache by version in the filename (skip the download if already present), then +# point the fixed-name symlink — the build's input — at this version's image. +if [[ ! -f "$IMG" ]]; then + curl --fail --silent --show-error --location "$UPDATE_IMG_URL" -o "$WORKSPACE/$IMG" +else + echo " $IMG already present, skipping download." +fi +ln -sfn "$(basename "$IMG")" "$WORKSPACE/$LINK" + +echo "==> Downloading signed proposal ${proposal_id}..." +bazel run //rs/ic_os/build_tools/alternative_guestos -- download-signed-proposal \ + --proposal-id "$proposal_id" \ + --output "$WORKSPACE/$CBOR" + +echo "==> Building SEV recovery image..." +bazel build "$TARGET"