diff --git a/scripts/run-push-containers b/scripts/run-push-containers
index 9f6f2fa..debf83f 100755
--- a/scripts/run-push-containers
+++ b/scripts/run-push-containers
@@ -17,7 +17,14 @@
 #   CONTAINER_AUTH_CONFIG    - registry auth file staged into the guest
 #   CONTAINER_COSIGN_KEY     - cosign private key path; when set, each
 #                              successfully pushed image is signed with
-#                              cosign (otherwise signing is skipped)
+#                              cosign (otherwise signing is skipped). Also
+#                              gates SPDX SBOM attestation: each pushed image
+#                              gets a 'cosign attest --type spdxjson'
+#                              attestation of its SPDX SBOM (per-arch on the
+#                              multi-arch path, top manifest on single-arch).
+#   CONTAINER_COSIGN_PUB     - cosign public key path; when set, each
+#                              attestation is verified with
+#                              'cosign verify-attestation' after it is made
 #
 
 import subprocess
@@ -59,6 +66,9 @@ if not auth_config:
 # Signing is conditional: only when CONTAINER_COSIGN_KEY points at a cosign
 # private key. When unset, no cosign sysroot is prepared and no signing runs.
 cosign_key = utils.getconfigvar("CONTAINER_COSIGN_KEY", ourconfig, args.target, args.stepnum)
+# Public key for verifying attestations; when unset, attestations are made but
+# not verified.
+cosign_pub = utils.getconfigvar("CONTAINER_COSIGN_PUB", ourconfig, args.target, args.stepnum)
 
 utils.printheader("Pushing container images %s" % list(container_images.keys()))
 
@@ -193,6 +203,13 @@ sys.stdout.write(base64.b64decode(t).decode() if t else "")' "%s" "$_host")
         login_block,
         "    _COSIGN_READY=1",
         "}",
+        # Resolve the lone manifest digest of a single-arch reference. Correct
+        # only when there is no manifest list (single-arch / child manifest);
+        # the multi-arch path passes explicit digests instead. oe-run-native
+        # prints 'Getting sysroot...' to stdout, so grep the digest out.
+        "resolve_digest() {",
+        "    oe-run-native skopeo-native skopeo inspect --authfile %s --format '{{.Digest}}' docker://$1 | grep -oE 'sha256:[0-9a-f]{64}' | tail -n1" % auth_config,
+        "}",
         # Track digests already signed in this run so the same artefact is
         # signed only once (see sign_image).
         "_SIGNED_REFS=\"\"",
@@ -205,7 +222,7 @@ sys.stdout.write(base64.b64decode(t).decode() if t else "")' "%s" "$_host")
         "    prepare_skopeo",
         "    prepare_cosign",
         "    _SIG_DIGEST=\"$2\"",
-        "    if [ -z \"$_SIG_DIGEST\" ]; then _SIG_DIGEST=$(oe-run-native skopeo-native skopeo inspect --authfile %s --format '{{.Digest}}' docker://$1 | grep -oE 'sha256:[0-9a-f]{64}' | tail -n1); fi" % auth_config,
+        "    if [ -z \"$_SIG_DIGEST\" ]; then _SIG_DIGEST=$(resolve_digest $1); fi",
         "    if [ -z \"$_SIG_DIGEST\" ]; then echo \"WARNING: could not resolve digest for $1, skipping cosign sign\"; return 0; fi",
         "    _SIG_REF=\"${1%:*}@${_SIG_DIGEST}\"",
         "    case \" $_SIGNED_REFS \" in *\" $_SIG_REF \"*) return 0 ;; esac",
@@ -222,6 +239,46 @@ sys.stdout.write(base64.b64decode(t).decode() if t else "")' "%s" "$_host")
         "    }",
         "    echo \"$_sign_out\"",
         "}",
+        # Make sure an image's SPDX SBOM exists before we attest it. $1 is the
+        # expected spdx.json path, $2 the bitbake target that produces it
+        # (mc:<mc>:<recipe> on the multi-arch path, plain <recipe> single-arch).
+        # create_image_sbom_spdx runs by default for single-arch builds, so the
+        # file is normally already there; the multi-arch index recipe doesn't
+        # pull in the per-arch SBOM tasks, so we generate them on demand.
+        "ensure_spdx() {",
+        "    if [ ! -e \"$1\" ]; then",
+        "        echo \"SPDX SBOM $1 missing, generating: bitbake -c create_image_sbom_spdx $2\"",
+        "        bitbake -c create_image_sbom_spdx $2 || true",
+        "    fi",
+        "    if [ ! -e \"$1\" ]; then echo \"WARNING: SPDX SBOM $1 still missing, skipping attestation\"; return 1; fi",
+        "}",
+        # Track refs already attested so the same digest is attested once.
+        "_ATTESTED_REFS=\"\"",
+        # Attest an SPDX SBOM against a digest-pinned image ref. $1 =
+        # <registry>/<image>@sha256:...; $2 = spdx.json path. Idempotent: a
+        # transparency-log conflict on rebuilds is treated as success. When a
+        # public key is configured the attestation is verified afterwards.
+        "attest_image() {",
+        "    prepare_skopeo",
+        "    prepare_cosign",
+        "    case \" $_ATTESTED_REFS \" in *\" $1 \"*) return 0 ;; esac",
+        "    _ATTESTED_REFS=\"$_ATTESTED_REFS $1\"",
+        "    _att_out=$(oe-run-native cosign-native cosign attest --key %s --type spdxjson --predicate \"$2\" \"$1\" 2>&1) || {" % cosign_key,
+        "        case \"$_att_out\" in",
+        "            *\"already exists\"*|*createLogEntryConflict*) echo \"cosign: attestation for $1 already in transparency log\" ;;",
+        "            *) echo \"$_att_out\" >&2; return 1 ;;",
+        "        esac",
+        "    }",
+        "    echo \"$_att_out\"",
+    ]
+    if cosign_pub:
+        script += [
+            "    oe-run-native cosign-native cosign verify-attestation --key %s --type spdxjson \"$1\" >/dev/null \\" % cosign_pub,
+            "        && echo \"cosign: verified attestation for $1\" \\",
+            "        || { echo \"ERROR: attestation verification failed for $1\" >&2; return 1; }",
+        ]
+    script += [
+        "}",
     ]
 
 tag_cmds = utils.getconfiglist("CONTAINER_TAG_CMDS", ourconfig, args.target, args.stepnum)
@@ -259,6 +316,13 @@ for recipe, image in container_images.items():
         "    *)   _DISTRO_VERSION=\"$_DISTRO_VERSION_RAW\" ;;",
         "esac",
         "_DEPLOY_DIR_IMAGE=$(echo \"$_BBENV\" | awk -F'\"' '/^DEPLOY_DIR_IMAGE=/{ print $2; exit }')",
+        # MACHINE locates the single-arch SPDX SBOM (<recipe>-<MACHINE>.rootfs.spdx.json).
+        "_MACHINE=$(echo \"$_BBENV\" | awk -F'\"' '/^MACHINE=/{ print $2; exit }')",
+        # TOPDIR and the oci-multiarch plain vars drive per-arch SBOM
+        # attestation below; the latter two are empty for non-multiarch recipes.
+        "_TOPDIR=$(echo \"$_BBENV\" | awk -F'\"' '/^TOPDIR=/{ print $2; exit }')",
+        "_OCI_MULTIARCH_RECIPE=$(echo \"$_BBENV\" | awk -F'\"' '/^OCI_MULTIARCH_RECIPE=/{ print $2; exit }')",
+        "_OCI_MULTIARCH_PLATFORMS=$(echo \"$_BBENV\" | awk -F'\"' '/^OCI_MULTIARCH_PLATFORMS=/{ print $2; exit }')",
         # Only set (non-empty) when the recipe inherits
         # oci-multiarch; doubles as the multiarch marker and
         # the OCI layout path for skopeo below.
@@ -318,6 +382,34 @@ for recipe, image in container_images.items():
         if cosign_key:
             script.append("            sign_image %s/%s:${_tag} \"$(cat \"$_DGSTFILE\")\"" % (registry, image))
         script.append("        done")
+    if cosign_key:
+        # Attest each child manifest with that arch's SPDX SBOM. The arch ->
+        # (multiconfig, machine) mapping lives in oci-multiarch.bbclass flags
+        # (OCI_MULTIARCH_MC/MACHINE[<arch>]); query them per platform with
+        # bitbake-getvar so an overridden mapping is still honoured. The child
+        # manifest digest is the per-arch source image's manifest digest, which
+        # the class copies verbatim into the index and skopeo copy --all pushes
+        # unchanged — so we attest <image>@<child-digest>.
+        script.append("        for _plat in $_OCI_MULTIARCH_PLATFORMS; do")
+        script.append("            _mc=$(bitbake-getvar -q -r %s --value -f $_plat OCI_MULTIARCH_MC)" % recipe)
+        script.append("            _machine=$(bitbake-getvar -q -r %s --value -f $_plat OCI_MULTIARCH_MACHINE)" % recipe)
+        script.append("            if [ -z \"$_mc\" ] || [ -z \"$_machine\" ]; then echo \"WARNING: no mc/machine for platform $_plat, skipping attestation\"; continue; fi")
+        script.append("            _pdir=${_TOPDIR}/tmp-${_mc}/deploy/images/${_machine}")
+        script.append("            _mspdx=${_pdir}/${_OCI_MULTIARCH_RECIPE}-${_machine}.rootfs.spdx.json")
+        # Locate the per-arch source OCI layout (same name patterns the class
+        # searches) and read its single manifest digest = the child digest.
+        script.append("            _mdig=\"\"")
+        script.append("            for _oci in ${_OCI_MULTIARCH_RECIPE}-latest-oci ${_OCI_MULTIARCH_RECIPE}-${_machine}-latest-oci ${_OCI_MULTIARCH_RECIPE}-oci; do")
+        script.append("                if [ -e \"${_pdir}/${_oci}/index.json\" ]; then")
+        script.append("                    _mdig=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))[\"manifests\"][0][\"digest\"])' \"${_pdir}/${_oci}/index.json\")")
+        script.append("                    break")
+        script.append("                fi")
+        script.append("            done")
+        script.append("            if [ -z \"$_mdig\" ]; then echo \"WARNING: no source OCI index for $_plat under $_pdir, skipping attestation\"; continue; fi")
+        script.append("            ensure_spdx \"$_mspdx\" \"mc:${_mc}:${_OCI_MULTIARCH_RECIPE}\" || continue")
+        for registry in registries:
+            script.append("            attest_image %s/%s@${_mdig} \"$_mspdx\"" % (registry, image))
+        script.append("        done")
     script.append("    fi")
     script.append("else")
     # Single-arch: import the ${recipe}-latest-oci artefact into the memres VM
@@ -327,6 +419,12 @@ for recipe, image in container_images.items():
     script.append("        echo \"WARNING: %s did not build (no OCI image at $_OCI_IMAGE), skipping push\"" % recipe)
     script.append("    else")
     script.append("        start_vm")
+    if cosign_key:
+        # create_image_sbom_spdx runs by default for single-arch image builds,
+        # so the SBOM is normally already deployed; ensure_spdx regenerates it
+        # only if absent. Single manifest -> attest the digest the tag resolves
+        # to (resolve_digest, also used by sign_image).
+        script.append("        _SPDX=${_DEPLOY_DIR_IMAGE}/%s-${_MACHINE}.rootfs.spdx.json" % recipe)
     for registry in registries:
         script += [
             "        for _tag in $_TAGS; do",
@@ -335,6 +433,10 @@ for recipe, image in container_images.items():
         ]
         if cosign_key:
             script.append("            sign_image %s/%s:${_tag}" % (registry, image))
+            script.append("            if ensure_spdx \"$_SPDX\" \"%s\"; then" % recipe)
+            script.append("                _ADIG=$(resolve_digest %s/%s:${_tag})" % (registry, image))
+            script.append("                [ -n \"$_ADIG\" ] && attest_image %s/%s@${_ADIG} \"$_SPDX\"" % (registry, image))
+            script.append("            fi")
         script.append("        done")
     script.append("    fi")
     script.append("fi")
