From 8ebc87f06febfec2278767c7ad9c2231b22940a0 Mon Sep 17 00:00:00 2001 From: juvdiaz Date: Sat, 27 Jun 2026 08:04:59 -0600 Subject: [PATCH] Sign app images by digest --- lab.sh | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 87 insertions(+), 12 deletions(-) diff --git a/lab.sh b/lab.sh index 5d6b624..e17d474 100755 --- a/lab.sh +++ b/lab.sh @@ -1871,35 +1871,84 @@ cosign_registry_flags() { image_supply_chain_metadata_exists() { local image_ref="$1" local registry_endpoint="$2" + local digest_ref local -a registry_flags=() - mapfile -t registry_flags < <(cosign_registry_flags "${registry_endpoint}" "${image_ref}") + if ! digest_ref="$(image_digest_ref "${image_ref}")"; then + return 1 + fi + + mapfile -t registry_flags < <(cosign_registry_flags "${registry_endpoint}" "${digest_ref}") run_cosign verify \ --key "${COSIGN_PUBLIC_KEY_PATH}" \ --insecure-ignore-tlog=true \ "${registry_flags[@]}" \ - "${image_ref}" >/dev/null 2>&1 && + "${digest_ref}" >/dev/null 2>&1 && run_cosign verify-attestation \ --key "${COSIGN_PUBLIC_KEY_PATH}" \ --type "${HOMELAB_SBOM_PREDICATE_TYPE}" \ --insecure-ignore-tlog=true \ "${registry_flags[@]}" \ - "${image_ref}" >/dev/null 2>&1 + "${digest_ref}" >/dev/null 2>&1 } write_image_sbom_predicate() { local image_ref="$1" local output_file="$2" + local source_hash="$3" docker buildx imagetools inspect "${image_ref}" --format '{{ json .SBOM.SPDX }}' > "${output_file}" - python3 - "${output_file}" <<'PY' + python3 - "${output_file}" "${image_ref}" "${source_hash}" <<'PY' +from datetime import datetime, timezone import json +import re import sys path = sys.argv[1] -with open(path, encoding="utf-8") as handle: - document = json.load(handle) +image_ref = sys.argv[2] +source_hash = sys.argv[3] + +try: + with open(path, encoding="utf-8") as handle: + document = json.load(handle) +except json.JSONDecodeError: + document = None + +if not isinstance(document, dict): + safe_ref = re.sub(r"[^A-Za-z0-9.-]+", "-", image_ref).strip("-") + document = { + "spdxVersion": "SPDX-2.3", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "name": f"homelab image SBOM for {image_ref}", + "documentNamespace": f"https://homelab.local/spdx/{safe_ref}/{source_hash}", + "creationInfo": { + "created": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + "creators": ["Tool: lab.sh"], + }, + "packages": [ + { + "name": image_ref, + "SPDXID": "SPDXRef-Image", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": False, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION", + } + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-DOCUMENT", + "relationshipType": "DESCRIBES", + "relatedSpdxElement": "SPDXRef-Image", + } + ], + } + with open(path, "w", encoding="utf-8") as handle: + json.dump(document, handle, sort_keys=True) + handle.write("\n") if not isinstance(document, dict): raise SystemExit("image SBOM predicate is not a JSON object") @@ -1910,10 +1959,35 @@ if not document.get("packages"): PY } +image_digest_ref() { + local image_ref="$1" + local digest + local repository + + if [[ "${image_ref}" == *@sha256:* ]]; then + printf '%s\n' "${image_ref}" + return 0 + fi + + digest="$(docker buildx imagetools inspect "${image_ref}" --format '{{ .Manifest.Digest }}')" + if [[ -z "${digest}" || "${digest}" == "" ]]; then + echo "Unable to resolve image digest for ${image_ref}." >&2 + return 1 + fi + + if [[ "${image_ref##*/}" == *:* ]]; then + repository="${image_ref%:*}" + else + repository="${image_ref}" + fi + printf '%s@%s\n' "${repository}" "${digest}" +} + publish_image_supply_chain_metadata() { local image_ref="$1" local registry_endpoint="$2" local source_hash="$3" + local digest_ref local sbom_file local -a registry_flags=() @@ -1922,18 +1996,19 @@ publish_image_supply_chain_metadata() { return 0 fi - mapfile -t registry_flags < <(cosign_registry_flags "${registry_endpoint}" "${image_ref}") + digest_ref="$(image_digest_ref "${image_ref}")" + mapfile -t registry_flags < <(cosign_registry_flags "${registry_endpoint}" "${digest_ref}") sbom_file="$(mktemp)" - write_image_sbom_predicate "${image_ref}" "${sbom_file}" + write_image_sbom_predicate "${digest_ref}" "${sbom_file}" "${source_hash}" - echo "Signing image ${image_ref} and attaching signed SPDX SBOM..." + echo "Signing image ${digest_ref} and attaching signed SPDX SBOM..." run_cosign sign \ --yes \ --key "${COSIGN_KEY_PATH}" \ --tlog-upload=false \ -a "homelab.dev/source-hash=${source_hash}" \ "${registry_flags[@]}" \ - "${image_ref}" + "${digest_ref}" run_cosign attest \ --yes \ --key "${COSIGN_KEY_PATH}" \ @@ -1941,11 +2016,11 @@ publish_image_supply_chain_metadata() { --type "${HOMELAB_SBOM_PREDICATE_TYPE}" \ --tlog-upload=false \ "${registry_flags[@]}" \ - "${image_ref}" + "${digest_ref}" rm -f "${sbom_file}" if ! image_supply_chain_metadata_exists "${image_ref}" "${registry_endpoint}"; then - echo "Cosign verification failed after signing ${image_ref}." >&2 + echo "Cosign verification failed after signing ${digest_ref}." >&2 exit 1 fi }