diff --git a/bazel/osimage/BUILD.bazel b/bazel/osimage/BUILD.bazel new file mode 100644 index 0000000000..1953012ca4 --- /dev/null +++ b/bazel/osimage/BUILD.bazel @@ -0,0 +1 @@ +exports_files(["upload_os_images.sh.in"]) diff --git a/bazel/osimage/upload_os_images.bzl b/bazel/osimage/upload_os_images.bzl new file mode 100644 index 0000000000..4d19841325 --- /dev/null +++ b/bazel/osimage/upload_os_images.bzl @@ -0,0 +1,104 @@ +""" Bazel rule for uploading a set of OS images to cloud providers. """ + +def _upload_os_images_impl(ctx): + executable = ctx.actions.declare_file("upload_os_images_%s.sh" % ctx.label.name) + files = [] + files.extend(ctx.files.image_dirs) + files.append(ctx.file._version) + files.append(ctx.file._upload_cli) + files.append(ctx.file._measured_boot) + files.append(ctx.file._uplosi) + files.append(ctx.file._dissect_toolchain) + files.append(ctx.file._cosign) + files.append(ctx.file._rekor_cli) + files.append(ctx.file._parallel) + raw_image_paths = [] + for image_dir in ctx.files.image_dirs: + raw_image_paths.append("%s/constellation.raw" % image_dir.short_path) + substitutions = { + "@@COSIGN@@": ctx.executable._cosign.short_path, + "@@FILES@@": " ".join(raw_image_paths), + "@@MEASURED_BOOT@@": ctx.executable._measured_boot.short_path, + "@@PARALLEL@@": ctx.executable._parallel.short_path, + "@@REKOR_CLI@@": ctx.executable._rekor_cli.short_path, + "@@UPLOAD_CLI@@": ctx.executable._upload_cli.short_path, + "@@UPLOSI@@": ctx.executable._uplosi.short_path, + "@@DISSECT_TOOLCHAIN@@": ctx.executable._dissect_toolchain.short_path, + "@@VERSION@@": ctx.file._version.short_path, + } + ctx.actions.expand_template( + template = ctx.file._upload_sh_tpl, + output = executable, + is_executable = True, + substitutions = substitutions, + ) + runfiles = ctx.runfiles(files = files) + runfiles = runfiles.merge(ctx.attr._uplosi[DefaultInfo].data_runfiles) + runfiles = runfiles.merge(ctx.attr._dissect_toolchain[DefaultInfo].data_runfiles) + runfiles = runfiles.merge(ctx.attr._cosign[DefaultInfo].data_runfiles) + runfiles = runfiles.merge(ctx.attr._rekor_cli[DefaultInfo].data_runfiles) + runfiles = runfiles.merge(ctx.attr._parallel[DefaultInfo].data_runfiles) + runfiles = runfiles.merge(ctx.attr._upload_cli[DefaultInfo].data_runfiles) + runfiles = runfiles.merge(ctx.attr._measured_boot[DefaultInfo].data_runfiles) + + return DefaultInfo(executable = executable, runfiles = runfiles) + +upload_os_images = rule( + implementation = _upload_os_images_impl, + attrs = { + "image_dirs": attr.label_list( + doc = "List of directories containing OS images to upload.", + ), + "_cosign": attr.label( + default = Label("@cosign//:bin/cosign"), + allow_single_file = True, + executable = True, + cfg = "exec", + ), + "_measured_boot": attr.label( + default = Label("//image/measured-boot/cmd"), + allow_single_file = True, + executable = True, + cfg = "exec", + ), + "_parallel": attr.label( + default = Label("@parallel//:bin/parallel"), + allow_single_file = True, + executable = True, + cfg = "exec", + ), + "_rekor_cli": attr.label( + default = Label("@rekor-cli//:bin/rekor-cli"), + allow_single_file = True, + executable = True, + cfg = "exec", + ), + "_upload_cli": attr.label( + default = Label("//image/upload"), + allow_single_file = True, + executable = True, + cfg = "exec", + ), + "_upload_sh_tpl": attr.label( + default = "upload_os_images.sh.in", + allow_single_file = True, + ), + "_uplosi": attr.label( + default = Label("@uplosi//:bin/uplosi"), + allow_single_file = True, + executable = True, + cfg = "exec", + ), + "_dissect_toolchain": attr.label( + default = Label("@systemd//:bin/systemd-dissect"), + allow_single_file = True, + executable = True, + cfg = "exec", + ), + "_version": attr.label( + default = Label("//bazel/settings:tag"), + allow_single_file = True, + ), + }, + executable = True, +) diff --git a/bazel/osimage/upload_os_images.sh.in b/bazel/osimage/upload_os_images.sh.in new file mode 100644 index 0000000000..ffaec952f2 --- /dev/null +++ b/bazel/osimage/upload_os_images.sh.in @@ -0,0 +1,144 @@ +#!/usr/bin/env bash + +set -euo pipefail +shopt -s inherit_errexit + +# This script handles the upload of OS images and their corresponding image info. + +POSITIONAL_ARGS=() + +ref="" +upload_signed_measurements=0 +fake_sign=0 + +while [[ $# -gt 0 ]]; do + case $1 in + --ref) + ref="$2" + shift # past argument + shift # past value + ;; + --upload-measurements) + upload_signed_measurements=1 + shift # past argument + ;; + --fake-sign) + fake_sign=1 + shift # past argument + ;; + -*) + echo "Unknown option $1" + exit 1 + ;; + *) + POSITIONAL_ARGS+=("$1") # save positional arg + shift # past argument + ;; + esac +done + +set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters + +if [[ $# -ne 0 ]]; then + echo "Unknown positional arguments: $*" + exit 1 +fi + +if [[ -z ${ref} ]]; then + echo "Missing required argument --ref" + exit 1 +fi + +version_file=$(realpath @@VERSION@@) +stat "${version_file}" >> /dev/null +version=$(cat "${version_file}") + +uplosi=$(realpath @@UPLOSI@@) +stat "${uplosi}" >> /dev/null + +systemd_dissect=$(realpath @@DISSECT_TOOLCHAIN@@) +stat "${systemd_dissect}" >> /dev/null +export DISSECT_TOOLCHAIN="${systemd_dissect}" + +cosign=$(realpath @@COSIGN@@) +stat "${cosign}" >> /dev/null + +rekor_cli=$(realpath @@REKOR_CLI@@) +stat "${rekor_cli}" >> /dev/null + +upload_cli=$(realpath @@UPLOAD_CLI@@) +stat "${upload_cli}" >> /dev/null + +measured_boot=$(realpath @@MEASURED_BOOT@@) +stat "${measured_boot}" >> /dev/null + +parallel=$(realpath @@PARALLEL@@) +stat "${parallel}" >> /dev/null + +FILES=(@@FILES@@) + +workspace=$(mktemp -d) +# shellcheck disable=SC2064 +trap "rm -rf ${workspace}" EXIT + +echo Uploading "${#FILES[@]}" OS images. This may take a while... >&2 + +"${parallel}" --will-cite \ + "${upload_cli}" uplosi \ + --uplosi-path "${uplosi}" \ + --version "${version}" \ + --ref "${ref}" \ + --raw-image {} \ + --out "${workspace}/image-upload-{#}.json" \ + ::: "${FILES[@]}" + +"${upload_cli}" info "${workspace}/"image-upload-*.json + +if [[ ${upload_signed_measurements} -eq 0 ]]; then + echo "Skipping signed measurements upload. Enable by setting --upload-measurements" >&2 + exit 0 +fi + +echo Uploading signed measurements. This requires sudo and a signing key. >&2 +i=1 +for file in "${FILES[@]}"; do + combined_name=$(basename "$(dirname "${file}")") + IFS="_" read -r csp attestation_variant stream <<< "${combined_name}" + sudo -E "${measured_boot}" "${file}" "${workspace}/pcrs-${i}.json" + sudo chown "$(id -u -n)" "${workspace}/pcrs-${i}.json" + "${upload_cli}" measurements envelope \ + --in "${workspace}/pcrs-${i}.json" \ + --out "${workspace}/pcrs-${i}.json" \ + --version "ref/${ref}/stream/${stream}/${version}" \ + --csp "${csp}" \ + --attestation-variant "${attestation_variant}" + i=$((i + 1)) +done + +"${upload_cli}" measurements merge \ + --out "${workspace}/measurements.json" \ + "${workspace}"/pcrs-*.json + +if [[ ${fake_sign} -eq 1 ]]; then + echo "Skipping signing of measurements and using fake signature instead (--fake-sign is set)." >&2 + echo "THOSE MEASUREMENTS BELONG TO A DEBUG IMAGE. THOSE ARE NOT SINGED BY ANY KEY." > "${workspace}/measurements.json.sig" +else + # shellcheck disable=SC2016 + echo 'Creating real signature with keys referenced in $COSIGN_PUBLIC_KEY_PATH, $COSIGN_PRIVATE_KEY and $COSIGN_PASSWORD. Set "--fake-sign" for debugging purposes.' >&2 + # Enabling experimental mode also publishes signature to Rekor + COSIGN_EXPERIMENTAL=1 "${cosign}" sign-blob --key env://COSIGN_PRIVATE_KEY \ + "${workspace}/measurements.json" > "${workspace}/measurements.json.sig" + # Verify - As documentation & check + # Local Signature (input: artifact, key, signature) + "${cosign}" verify-blob --key "${COSIGN_PUBLIC_KEY_PATH}" \ + --signature "${workspace}/measurements.json.sig" \ + "${workspace}/measurements.json" + # Transparency Log Signature (input: artifact, key) + uuid=$("${rekor_cli}" search --artifact "${workspace}/measurements.json" | tail -n 1) + sig=$("${rekor_cli}" get --uuid="${uuid}" --format=json | jq -r .Body.HashedRekordObj.signature.content) + "${cosign}" verify-blob --key "${COSIGN_PUBLIC_KEY_PATH}" --signature <(echo "${sig}") "${workspace}/measurements.json" +fi + +"${upload_cli}" measurements upload \ + --measurements "${workspace}/measurements.json" \ + --signature "${workspace}/measurements.json.sig"