From 99659675543bfcff36b1a7ed61ed530f6f79a39f Mon Sep 17 00:00:00 2001 From: Ben Cressey Date: Tue, 9 May 2023 23:53:05 +0000 Subject: [PATCH] build: add scripts to create Secure Boot profiles For Secure Boot, various certificates, keys, and configuration files are needed to sign binaries and register images. Provide two scripts to simplify the process of generating the correct artifacts. The "local" version of the script is only meant for non-production use, for example by individual developers or by automated CI testing, where the variant builds must support the feature but do not need to be maintained indefinitely. The "aws" version of the script expects various AWS resources such as private CAs and managed keys to be available. This can be costly and only makes sense for official builds of supported variants. Signed-off-by: Ben Cressey --- .gitignore | 1 + sbkeys/generate-aws-sbkeys | 347 +++++++++++++++++++++++++++++++++++ sbkeys/generate-local-sbkeys | 166 +++++++++++++++++ 3 files changed, 514 insertions(+) create mode 100755 sbkeys/generate-aws-sbkeys create mode 100755 sbkeys/generate-local-sbkeys diff --git a/.gitignore b/.gitignore index e995a802549..81553e84e95 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ /*.pem /keys /roles +/sbkeys/**/ /Licenses.toml /licenses *.run diff --git a/sbkeys/generate-aws-sbkeys b/sbkeys/generate-aws-sbkeys new file mode 100755 index 00000000000..fd2b172d10c --- /dev/null +++ b/sbkeys/generate-aws-sbkeys @@ -0,0 +1,347 @@ +#!/usr/bin/env bash + +# Helper script for running commands to generate Secure Boot files. + +set -euo pipefail + +usage() { + cat >&2 <&2 + exit 2 + fi +} + +parse_args() { + while [ ${#} -gt 0 ] ; do + case "${1}" in + --help ) usage; exit 0 ;; + --sdk-image ) shift; SDK_IMAGE="${1}" ;; + --aws-region ) shift; AWS_REGION="${1}" ;; + --pk-ca ) shift; PK_CA="${1}" ;; + --kek-ca ) shift; KEK_CA="${1}" ;; + --db-ca ) shift; DB_CA="${1}" ;; + --vendor-ca ) shift; VENDOR_CA="${1}" ;; + --shim-sign-key ) shift; SHIM_SIGN_KEY="${1}" ;; + --code-sign-key ) shift; CODE_SIGN_KEY="${1}" ;; + --config-sign-key ) shift; CONFIG_SIGN_KEY="${1}" ;; + --output-dir ) shift; OUTPUT_DIR="${1}" ;; + *) ;; + esac + shift + done + + # Required arguments + required_arg "--aws-region" "${AWS_REGION:-}" + required_arg "--pk-ca" "${PK_CA:-}" + required_arg "--kek-ca" "${KEK_CA:-}" + required_arg "--db-ca" "${DB_CA:-}" + required_arg "--vendor-ca" "${VENDOR_CA:-}" + required_arg "--shim-sign-key" "${SHIM_SIGN_KEY:-}" + required_arg "--code-sign-key" "${CODE_SIGN_KEY:-}" + required_arg "--config-sign-key" "${CONFIG_SIGN_KEY:-}" + required_arg "--output-dir" "${OUTPUT_DIR:-}" +} + +parse_args "${@}" + +# To avoid needing separate scripts to parse args and launch the SDK container, +# the logic to generate the profile is found below the separator. Copy that to +# a temporary file so it can be executed using the desired method. +PRELUDE_END=$(awk '/=\^\.\.\^=/ { print NR+1; exit 0; }' "${0}") +SBKEYS_SCRIPT="$(mktemp)" +AWS_KMS_PKCS11_CONF="$(mktemp)" +cleanup() { + rm -f "${SBKEYS_SCRIPT}" "${AWS_KMS_PKCS11_CONF}" +} +trap 'cleanup' EXIT +tail -n +"${PRELUDE_END}" "${0}" >"${SBKEYS_SCRIPT}" +chmod +x "${SBKEYS_SCRIPT}" + +cat < "${AWS_KMS_PKCS11_CONF}" +{ + "slots": [ + { + "label": "shim-sign-key", + "kms_key_id": "${SHIM_SIGN_KEY}", + "aws_region": "${AWS_REGION}" + }, + { + "label": "code-sign-key", + "kms_key_id": "${CODE_SIGN_KEY}", + "aws_region": "${AWS_REGION}" + }, + { + "label": "config-sign-key", + "kms_key_id": "${CONFIG_SIGN_KEY}", + "aws_region": "${AWS_REGION}" + } + ] +} +EOF + +# Create the output directory with the current user, rather than letting Docker +# create it as a root-owned directory. +mkdir -p "${OUTPUT_DIR}" + +if [ -n "${SDK_IMAGE:-}" ] ; then + docker run -a stdin -a stdout -a stderr --rm \ + --user "$(id -u):$(id -g)" \ + --security-opt label:disable \ + -v "${OUTPUT_DIR}":/tmp/output \ + -v "${SBKEYS_SCRIPT}":/tmp/sbkeys \ + -v "${AWS_KMS_PKCS11_CONF}":/tmp/aws-kms-pkcs11-conf \ + ${AWS_ACCESS_KEY_ID:+-e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID} \ + ${AWS_SECRET_ACCESS_KEY:+-e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY} \ + ${AWS_SESSION_TOKEN:+-e AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN} \ + -e AWS_REGION="${AWS_REGION}" \ + -e AWS_DEFAULT_REGION="${AWS_REGION}" \ + -e PK_CA="${PK_CA}" \ + -e KEK_CA="${KEK_CA}" \ + -e DB_CA="${DB_CA}" \ + -e VENDOR_CA="${VENDOR_CA}" \ + -e SHIM_SIGN_KEY="${SHIM_SIGN_KEY}" \ + -e CODE_SIGN_KEY="${CODE_SIGN_KEY}" \ + -e CONFIG_SIGN_KEY="${CONFIG_SIGN_KEY}" \ + -e AWS_KMS_PKCS11_CONF="/tmp/aws-kms-pkcs11-conf" \ + -e OUTPUT_DIR="/tmp/output" \ + -w /tmp \ + "${SDK_IMAGE}" bash /tmp/sbkeys +else + export PK_CA KEK_CA DB_CA VENDOR_CA + export CODE_SIGN_KEY CONFIG_SIGN_KEY SHIM_SIGN_KEY + export AWS_REGION AWS_KMS_PKCS11_CONF OUTPUT_DIR + bash "${SBKEYS_SCRIPT}" +fi + +exit + +# =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= +set -euo pipefail + +WORKDIR="$(mktemp -d)" +cd "${WORKDIR}" +cleanup() { + rm -rf "${WORKDIR}" +} +trap 'cleanup' EXIT + +export XDG_CONFIG_HOME="${WORKDIR}/.config" +mkdir -p "${XDG_CONFIG_HOME}/aws-kms-pkcs11" +cp "${AWS_KMS_PKCS11_CONF}" "${XDG_CONFIG_HOME}/aws-kms-pkcs11/config.json" + +export AWS_DEFAULT_OUTPUT="text" +export AWS_KMS_PKCS11_DEBUG=1 +export PKCS11_MODULE_PATH="/usr/lib64/pkcs11/aws_kms_pkcs11.so" + +# Fetch CA certificates. +getcacert() { + local arn ca + arn="${1:?}" + ca="${2:?}" + aws acm-pca get-certificate-authority-certificate \ + --certificate-authority-arn "${arn}" \ + --query 'Certificate' > "${ca}.crt" +} + +getcacert "${PK_CA}" "PK" +getcacert "${KEK_CA}" "KEK" +getcacert "${DB_CA}" "db" +getcacert "${VENDOR_CA}" "vendor" + +# Add X.509 extension for code signing. +cat <<'EOF' > codesign.json +{ + "Extensions": { + "ExtendedKeyUsage": [ + { + "ExtendedKeyUsageType": "CODE_SIGNING" + }, + { + "ExtendedKeyUsageObjectIdentifier": "1.3.6.1.4.1.311.10.3.6" + } + ] + } +} +EOF + +gencert() { + local key token cn ca_arn cert_arn + key="${1:?}" + token="${2:?}" + cn="${3:?}" + ca_arn="${4:?}" + + openssl req -new \ + -key "pkcs11:token=${token}" -keyform engine -engine pkcs11 \ + -subj "/CN=${cn}/" \ + -out "${key}.csr" + + cert_arn="$(\ + aws acm-pca issue-certificate \ + --certificate-authority-arn "${ca_arn}" \ + --template-arn arn:aws:acm-pca:::template/BlankEndEntityCertificate_APICSRPassthrough/V1 \ + --csr "fileb://${key}.csr" \ + --api-passthrough "file://codesign.json" \ + --signing-algorithm "SHA256WITHRSA" \ + --validity Value=5,Type="YEARS" \ + --idempotency-token "${key}" \ + --query 'CertificateArn')" + + aws acm-pca wait certificate-issued \ + --certificate-authority-arn "${ca_arn}" \ + --certificate-arn "${cert_arn}" + + aws acm-pca get-certificate \ + --certificate-authority-arn "${ca_arn}" \ + --certificate-arn "${cert_arn}" \ + --query 'Certificate' \ + > "${key}.crt" +} + +# Sign shim, GRUB, kernel, and GRUB config signing keys. +gencert shim-sign "shim-sign-key" "Bottlerocket Shim Signing Key" "${DB_CA}" +gencert code-sign "code-sign-key" "Bottlerocket Code Signing Key" "${VENDOR_CA}" +gencert config-sign "config-sign-key" "Bottlerocket Config Signing Key" "${VENDOR_CA}" + +# Encode the certs for the PKCS11 helper. +SHIM_SIGN_CERT="$(openssl x509 -in shim-sign.crt -outform der | openssl base64 -A)" +CODE_SIGN_CERT="$(openssl x509 -in code-sign.crt -outform der | openssl base64 -A)" +CONFIG_SIGN_CERT="$(openssl x509 -in config-sign.crt -outform der | openssl base64 -A)" + +# Reconfigure the PKCS11 helper for GPG. +cat < "${XDG_CONFIG_HOME}/aws-kms-pkcs11/config.json" +{ + "slots": [ + { + "label": "config-sign-key", + "kms_key_id": "${CONFIG_SIGN_KEY}", + "aws_region": "${AWS_REGION}", + "certificate": "${CONFIG_SIGN_CERT}" + } + ] +} +EOF + +# Ensure a clean GPG state. +export GNUPGHOME="${WORKDIR}" + +# Configure the GPG agent and smartcard daemon. +cat <> "${GNUPGHOME}/gpg-agent.conf" +scdaemon-program /usr/bin/gnupg-pkcs11-scd +EOF + +cat <> "${GNUPGHOME}/gnupg-pkcs11-scd.conf" +providers kms +provider-kms-library /usr/lib64/pkcs11/aws_kms_pkcs11.so +log-file /dev/null +EOF + +# Have GPG agent discover the key. +gpg --card-status +KEYGRIP=$(\ + find "${GNUPGHOME}"/private-keys-*.d -type f -name '*.key' -printf '%P' \ + | cut -d '.' -f1 | head -n1) + +# Import the config signing key into GPG. +# 13 Existing key +# ${KEYGRIP} Which key to edit +# e Toggle the encrypt capability off +# q Finished +# 0 Key does not expire +# Bottlerocket ... Real name +# Email address +# Comment +gpg --no-tty --expert --full-generate-key --command-fd 0 < config-sign.key + +# Generate EFI vars for use with EC2 or others. +GUID="$(uuidgen --random)" +virt-fw-vars \ + --set-pk "${GUID}" PK.crt \ + --add-kek "${GUID}" KEK.crt \ + --add-db "${GUID}" db.crt \ + --secure-boot \ + --output-json "efi-vars.json" + +virt-fw-vars \ + --set-json "efi-vars.json" \ + --output-aws "efi-vars.aws" + +# Create the final PKCS11 helper config. +cat < "kms-sign.json" +{ + "slots": [ + { + "label": "shim-sign-key", + "kms_key_id": "${SHIM_SIGN_KEY}", + "aws_region": "${AWS_REGION}", + "certificate": "${SHIM_SIGN_CERT}" + }, + { + "label": "code-sign-key", + "kms_key_id": "${CODE_SIGN_KEY}", + "aws_region": "${AWS_REGION}", + "certificate": "${CODE_SIGN_CERT}" + }, + { + "label": "config-sign-key", + "kms_key_id": "${CONFIG_SIGN_KEY}", + "aws_region": "${AWS_REGION}", + "certificate": "${CONFIG_SIGN_CERT}" + } + ] +} +EOF + +# Copy all expected files out. +cp -t "${OUTPUT_DIR}" \ + PK.crt \ + KEK.crt \ + db.crt \ + vendor.crt \ + kms-sign.json \ + config-sign.key \ + efi-vars.{aws,json} diff --git a/sbkeys/generate-local-sbkeys b/sbkeys/generate-local-sbkeys new file mode 100755 index 00000000000..5dbc2a06fd8 --- /dev/null +++ b/sbkeys/generate-local-sbkeys @@ -0,0 +1,166 @@ +#!/usr/bin/env bash + +# Helper script for running commands to generate Secure Boot files. + +set -euo pipefail + +usage() { + cat >&2 <&2 + exit 2 + fi +} + +parse_args() { + while [ ${#} -gt 0 ] ; do + case "${1}" in + --help ) usage; exit 0 ;; + --sdk-image ) shift; SDK_IMAGE="${1}" ;; + --output-dir ) shift; OUTPUT_DIR="${1}" ;; + *) ;; + esac + shift + done + + # Required arguments + required_arg "--output-dir" "${OUTPUT_DIR:-}" +} + +parse_args "${@}" + +# Create the output directory with the current user, rather than letting Docker +# create it as a root-owned directory. +mkdir -p "${OUTPUT_DIR}" + +# To avoid needing separate scripts to parse args and launch the SDK container, +# the logic to generate the profile is found below the separator. Copy that to +# a temporary file so it can be executed using the desired method. +PRELUDE_END=$(awk '/=\^\.\.\^=/ { print NR+1; exit 0; }' "${0}") +SBKEYS_SCRIPT="$(mktemp)" +cleanup() { + rm -f "${SBKEYS_SCRIPT}" +} +trap 'cleanup' EXIT +tail -n +"${PRELUDE_END}" "${0}" >"${SBKEYS_SCRIPT}" +chmod +x "${SBKEYS_SCRIPT}" + +if [ -n "${SDK_IMAGE:-}" ] ; then + docker run -a stdin -a stdout -a stderr --rm \ + --user "$(id -u):$(id -g)" \ + --security-opt label:disable \ + -v "${OUTPUT_DIR}":/tmp/output \ + -v "${SBKEYS_SCRIPT}":/tmp/sbkeys \ + -e OUTPUT_DIR="/tmp/output" \ + -w /tmp \ + "${SDK_IMAGE}" bash /tmp/sbkeys +else + export OUTPUT_DIR + bash "${SBKEYS_SCRIPT}" +fi + +exit + +# =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= +set -euo pipefail + +WORKDIR="$(mktemp -d)" +cd "${WORKDIR}" +cleanup() { + rm -rf "${WORKDIR}" +} +trap 'cleanup' EXIT + +genca() { + local ca cn + ca="${1:?}" + cn="${2:?}" + openssl req -newkey rsa:2048 \ + -batch -noenc -new -x509 -sha256 -days 3650 \ + -subj "/CN=${cn}/" \ + -keyout "${ca}.key" -out "${ca}.crt" +} + +genkey() { + local ca key cn + ca="${1:?}" + key="${2:?}" + cn="${3:?}" + openssl genrsa -verbose \ + -out "${key}.key" 2048 + + openssl req -new \ + -key "${key}.key" \ + -subj "/CN=${cn}/" \ + -out "${key}.csr" + + openssl req \ + -in "${key}.csr" \ + -CA "${ca}.crt" -CAkey "${ca}.key" \ + -config /dev/null \ + -days 3650 -x509 -sha256 -copy_extensions none \ + -addext "basicConstraints=CA:FALSE" \ + -addext "extendedKeyUsage=codeSigning,1.3.6.1.4.1.311.10.3.6" \ + -out "${key}.crt" +} + +# Generate local EFI CAs and signing keys. +genca PK "Bottlerocket Secure Boot Platform CA" +genca KEK "Bottlerocket Secure Boot Key Exchange CA" +genca db "Bottlerocket Secure Boot Database CA" +genca vendor "Bottlerocket Secure Boot Vendor CA" + +genkey db shim-sign "Bottlerocket Shim Signing Key" +genkey vendor code-sign "Bottlerocket Code Signing Key" + +# Generate GPG key for signing grub.cfg. +export GNUPGHOME="${WORKDIR}" +gpg --gen-key --batch < config-sign.key + +# Generate EFI vars for use with EC2 or others. +GUID="$(uuidgen --random)" +virt-fw-vars \ + --set-pk "${GUID}" PK.crt \ + --add-kek "${GUID}" KEK.crt \ + --add-db "${GUID}" db.crt \ + --secure-boot \ + --output-json "efi-vars.json" + +virt-fw-vars \ + --set-json "efi-vars.json" \ + --output-aws "efi-vars.aws" + +# Copy all expected files out. +cp -t "${OUTPUT_DIR}" \ + PK.{key,crt} \ + KEK.{key,crt} \ + db.{key,crt} \ + vendor.{key,crt} \ + shim-sign.{key,crt} \ + code-sign.{key,crt} \ + config-sign.key \ + efi-vars.{aws,json}