#!/bin/bash # SPDX-Version: 3.0 # SPDX-CreationInfo: 2025-10-11; WEIDNER, Marc S.; # SPDX-ExternalRef: GIT https://git.coresecret.dev/msw/CISS.debian.live.builder.git # SPDX-FileContributor: WEIDNER, Marc S.; Centurion Intelligence Consulting Agency # SPDX-FileCopyrightText: 2024-2025; WEIDNER, Marc S.; # SPDX-FileType: SOURCE # SPDX-License-Identifier: LicenseRef-CNCL-1.1 OR LicenseRef-CCLA-1.1 # SPDX-LicenseComment: This file is part of the CISS.debian.installer.secure framework. # SPDX-PackageName: CISS.debian.live.builder # SPDX-Security-Contact: security@coresecret.eu set -Ceuo pipefail printf "\e[95m🧪 '%s' starting ... \e[0m\n" "${0}" [[ -r /root/ciss_xdg_tmp.sh ]] && . /root/ciss_xdg_tmp.sh export DEBIAN_FRONTEND="noninteractive" export INITRD="No" declare SOPS_COSIGN_CERTIFICATE_IDENTITY_REGEXP="https://github.com/getsops" declare SOPS_COSIGN_CERTIFICATE_OIDC_ISSUER="https://token.actions.githubusercontent.com" ####################################### # Print a fatal error and abort the hook. # Globals: # None # Arguments: # 1: Message string # Returns: # None ####################################### die() { declare message="$1" printf "\e[91m❌ ERROR: %s \e[0m\n" "${message}" >&2 exit 43 } ####################################### # Require an executable tool. # Globals: # None # Arguments: # 1: Tool name # Returns: # 0: on success ####################################### require_tool() { declare tool_name="$1" command -v "${tool_name}" >/dev/null 2>&1 || die "Required tool not found: ${tool_name}" return 0 } ####################################### # Validate and normalize a SOPS semantic version. # Globals: # None # Arguments: # 1: SOPS version string # Outputs: # Normalized bare semantic version # Returns: # 0: on success ####################################### normalize_sops_version() { declare sops_version="${1#v}" [[ "${sops_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || \ die "Invalid SOPS version '${1}'. Expected '..' without prerelease metadata." printf '%s' "${sops_version}" return 0 } ####################################### # Download a mandatory release asset. # Globals: # None # Arguments: # 1: Asset URL # 2: Target filename # Returns: # 0: on success ####################################### download_required_asset() { declare asset_url="$1" declare target_file="$2" if ! curl -fsSLo "${target_file}" "${asset_url}"; then die "Failed to download required SOPS asset '${target_file}' from '${asset_url}'." fi [[ -s "${target_file}" ]] || die "Downloaded SOPS asset is empty: ${target_file}" return 0 } ####################################### # Download an optional release asset and distinguish absence from download errors. # Globals: # None # Arguments: # 1: Asset URL # 2: Target filename # Returns: # 0: asset was downloaded # 1: asset is absent upstream ####################################### download_optional_asset() { declare asset_url="$1" declare target_file="$2" declare http_code="" if ! http_code=$(curl -sSLo "${target_file}" -w '%{http_code}' "${asset_url}"); then rm -f -- "${target_file}" die "Failed to query optional SOPS asset '${target_file}' from '${asset_url}'." fi case "${http_code}" in 200) [[ -s "${target_file}" ]] || die "Optional SOPS asset is empty after HTTP 200: ${target_file}" return 0 ;; 404) rm -f -- "${target_file}" return 1 ;; *) rm -f -- "${target_file}" die "Unexpected HTTP status ${http_code} for optional SOPS asset '${target_file}' from '${asset_url}'." ;; esac } ####################################### # Verify the SOPS checksums file with Cosign. # Globals: # SOPS_COSIGN_CERTIFICATE_IDENTITY_REGEXP # SOPS_COSIGN_CERTIFICATE_OIDC_ISSUER # Arguments: # 1: Checksums filename # 2: Bundle filename # 3: Certificate filename # 4: Signature filename # Returns: # 0: on success ####################################### verify_sops_checksums_signature() { declare checksums_file="$1" declare bundle_file="$2" declare certificate_file="$3" declare signature_file="$4" if [[ -f "${bundle_file}" ]]; then printf "\e[95m[INFO] Verifying SOPS checksums with Cosign bundle: %s \e[0m\n" "${bundle_file}" cosign verify-blob "${checksums_file}" \ --bundle "${bundle_file}" \ --certificate-identity-regexp="${SOPS_COSIGN_CERTIFICATE_IDENTITY_REGEXP}" \ --certificate-oidc-issuer="${SOPS_COSIGN_CERTIFICATE_OIDC_ISSUER}" || \ die "SOPS checksum signature verification failed in bundle mode for '${checksums_file}' using '${bundle_file}'." return 0 fi if [[ -f "${certificate_file}" && -f "${signature_file}" ]]; then printf "\e[95m[INFO] Verifying SOPS checksums with Cosign split certificate/signature: %s %s \e[0m\n" "${certificate_file}" "${signature_file}" cosign verify-blob "${checksums_file}" \ --certificate "${certificate_file}" \ --signature "${signature_file}" \ --certificate-identity-regexp="${SOPS_COSIGN_CERTIFICATE_IDENTITY_REGEXP}" \ --certificate-oidc-issuer="${SOPS_COSIGN_CERTIFICATE_OIDC_ISSUER}" || \ die "SOPS checksum signature verification failed in legacy split mode for '${checksums_file}' using '${certificate_file}' and '${signature_file}'." return 0 fi if [[ -f "${certificate_file}" || -f "${signature_file}" ]]; then die "Incomplete legacy SOPS signature layout for '${checksums_file}'. Expected both '${certificate_file}' and '${signature_file}'." fi die "No supported SOPS checksum signature layout found for '${checksums_file}'. Expected bundle or split certificate/signature assets." } ####################################### # Verify the SOPS artifact checksum and ensure the expected artifact was covered. # Globals: # None # Arguments: # 1: Checksums filename # 2: Artifact filename # Returns: # 0: on success ####################################### verify_sops_artifact_checksum() { declare checksums_file="$1" declare artifact_file="$2" declare checksum_output="" if ! checksum_output=$(sha256sum -c "${checksums_file}" --ignore-missing 2>&1); then printf '%s\n' "${checksum_output}" >&2 die "SOPS artifact checksum verification failed for '${artifact_file}' using '${checksums_file}'." fi printf '%s\n' "${checksum_output}" if ! grep -Fxq "${artifact_file}: OK" <<< "${checksum_output}" && \ ! grep -Fxq "./${artifact_file}: OK" <<< "${checksum_output}"; then die "SOPS checksum verification did not cover expected artifact '${artifact_file}' from '${checksums_file}'." fi return 0 } ####################################### # Install SOPS from an upstream GitHub release after signature and checksum verification. # Globals: # CISS_SOPS_VERSION # Arguments: # None # Returns: # 0: on success ####################################### main() { require_tool curl require_tool cosign require_tool sha256sum declare sops_env="/root/sops.env" [[ -r "${sops_env}" ]] || die "Missing SOPS environment file: ${sops_env}" # shellcheck disable=SC1090 . "${sops_env}" declare ciss_sops_version ciss_sops_version=$(normalize_sops_version "${CISS_SOPS_VERSION:?CISS_SOPS_VERSION is not set}") declare architecture architecture="$(dpkg --print-architecture)" declare sops_tag="v${ciss_sops_version}" declare sops_file="" case "${architecture}" in amd64) sops_file="sops-${sops_tag}.linux.amd64" ;; arm64) sops_file="sops-${sops_tag}.linux.arm64" ;; *) die "Unsupported architecture '${architecture}' for SOPS version '${ciss_sops_version}'. Expected amd64 or arm64." ;; esac declare release_base_url="https://github.com/getsops/sops/releases/download/${sops_tag}" declare checksums_file="sops-${sops_tag}.checksums.txt" declare bundle_file="sops-${sops_tag}.checksums.sigstore.json" declare certificate_file="sops-${sops_tag}.checksums.pem" declare signature_file="sops-${sops_tag}.checksums.sig" declare bundle_available="false" declare certificate_available="false" declare signature_available="false" cd /tmp printf "\e[95m[INFO] Downloading SOPS %s asset: %s \e[0m\n" "${ciss_sops_version}" "${sops_file}" download_required_asset "${release_base_url}/${sops_file}" "${sops_file}" download_required_asset "${release_base_url}/${checksums_file}" "${checksums_file}" # shellcheck disable=SC2310 if download_optional_asset "${release_base_url}/${bundle_file}" "${bundle_file}"; then bundle_available="true" fi if [[ "${bundle_available}" == "false" ]]; then # shellcheck disable=SC2310 if download_optional_asset "${release_base_url}/${certificate_file}" "${certificate_file}"; then certificate_available="true" fi # shellcheck disable=SC2310 if download_optional_asset "${release_base_url}/${signature_file}" "${signature_file}"; then signature_available="true" fi if [[ "${certificate_available}" != "${signature_available}" ]]; then die "Incomplete legacy SOPS signature assets for version '${ciss_sops_version}'. Expected both '${certificate_file}' and '${signature_file}'." fi fi verify_sops_checksums_signature "${checksums_file}" "${bundle_file}" "${certificate_file}" "${signature_file}" verify_sops_artifact_checksum "${checksums_file}" "${sops_file}" install -m 0755 "${sops_file}" /usr/local/bin/sops sops --version >| /root/.ciss/cdlb/log/sops.log age --version >| /root/.ciss/cdlb/log/age.log rm -f -- "/tmp/${sops_file}" rm -f -- "/tmp/${checksums_file}" rm -f -- "/tmp/${bundle_file}" rm -f -- "/tmp/${certificate_file}" rm -f -- "/tmp/${signature_file}" if [[ -f /root/.config/sops/age/keys.txt ]]; then chmod 0400 /root/.config/sops/age/keys.txt fi printf "\e[92m✅ '%s' applied successfully. \e[0m\n" "${0}" return 0 } if [[ "${CISS_SOPS_TEST_MODE:-false}" != "true" ]]; then main "$@" exit 0 fi # vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=sh