diff --git a/meta/classes/create-spdx-3.0.bbclass b/meta/classes/create-spdx-3.0.bbclass
index 56fd01fd533..1ef6b15ac22 100644
--- a/meta/classes/create-spdx-3.0.bbclass
+++ b/meta/classes/create-spdx-3.0.bbclass
@@ -285,6 +285,7 @@ addtask do_create_recipe_sbom_setscene
 python spdx30_build_started_handler () {
     import oe.spdx30_tasks
     d = e.data.createCopy()
+    oe.spdx30_tasks.write_layers_spdx(d)
     oe.spdx30_tasks.write_bitbake_spdx(d)
 }
 
diff --git a/meta/classes/spdx-common.bbclass b/meta/classes/spdx-common.bbclass
index 40701730a69..b64537dfb48 100644
--- a/meta/classes/spdx-common.bbclass
+++ b/meta/classes/spdx-common.bbclass
@@ -89,6 +89,9 @@ SPDX_FILE_EXCLUDE_PATTERNS[doc] = "Space-separated list of Python regular \
     (no filtering). Example: \
     SPDX_FILE_EXCLUDE_PATTERNS = '\\.patch$ \\.diff$ /test/ \\.pyc$ \\.o$'"
 
+SPDX_REQUIRE_LAYER_ASSERTION ??= "0"
+SPDX_LAYER_REMOTE ??= "origin"
+
 python () {
     from oe.cve_check import extend_cve_status
     extend_cve_status(d)
diff --git a/meta/lib/oe/sbom30.py b/meta/lib/oe/sbom30.py
index 0f1f9281ad3..d2a3155b2b1 100644
--- a/meta/lib/oe/sbom30.py
+++ b/meta/lib/oe/sbom30.py
@@ -752,6 +752,16 @@ class ObjectSet(oe.spdx30.SHACLObjectSet):
 
         return bb_objset
 
+    def import_layers_objset(self):
+        deploy_dir_spdx = Path(self.d.getVar("DEPLOY_DIR_SPDX"))
+        layers_objset = load_jsonld(
+            self.d, deploy_dir_spdx / "layers.spdx.json", required=True
+        )
+        self.doc.import_.extend(layers_objset.doc.import_)
+        self.update(layers_objset.objects)
+
+        return layers_objset
+
     def import_bitbake_build(self):
         def find_bitbake_build(objset):
             return objset.find_filter(
@@ -793,6 +803,8 @@ class ObjectSet(oe.spdx30.SHACLObjectSet):
                 [build],
             )
 
+        layers = self.import_layers_objset()
+
         if self.d.getVar("SPDX_INCLUDE_BUILD_VARIABLES") == "1":
             for varname in sorted(self.d.keys()):
                 if varname.startswith("__"):
diff --git a/meta/lib/oe/spdx30_tasks.py b/meta/lib/oe/spdx30_tasks.py
index 1821dd7de4a..0a8f4b0e1a0 100644
--- a/meta/lib/oe/spdx30_tasks.py
+++ b/meta/lib/oe/spdx30_tasks.py
@@ -1220,6 +1220,80 @@ def create_package_spdx(d):
     oe.sbom30.write_recipe_jsonld_doc(d, common_objset, "common-package", deploydir)
 
 
+def git_remote_has_rev(path, remote, rev):
+    try:
+        branchlist, _ = bb.process.run(f"git branch --remotes --list '{remote}/*' --contains {rev}", cwd=path)
+    except bb.process.ExecutionError:
+        return False
+    return len(branchlist) > 0
+
+def get_layer_downloadLocation(d, l_path, l_name, l_branch, l_rev, l_ismodified):
+    from urllib.parse import urlparse, urlunparse
+
+    repo = oe.buildcfg.get_metadata_git_toplevel(l_path)
+
+    if not repo or not l_name or l_ismodified:
+        return "NOASSERTION"
+
+    remoteName = d.getVar(f"SPDX_LAYER_REMOTE_{l_name}") or d.getVar(f"SPDX_LAYER_REMOTE")
+    if not remoteName:
+        return "NOASSERTION"
+
+    # parse and reconstruct the url to drop potentially present username and password
+    remoteUrl = urlparse(oe.buildcfg.get_metadata_git_remote_url(repo, remoteName))
+    if not remoteUrl or remoteUrl.scheme not in ["https", "http", "git", "ssh"]:
+        return "NOASSERTION"
+    remoteUrl = urlunparse(remoteUrl)
+
+    # revision = d.getVar(f"SPDX_LAYER_CLAIM_REV_UNCHECKED_{l_name}")
+    # if not revision:
+    revision = l_rev
+    if revision == "<unknown>" or not git_remote_has_rev(repo, remoteName, revision):
+        return "NOASSERTION"
+
+    relpath = os.path.relpath(l_path, repo)
+
+    return f"git+{remoteUrl}@{revision}{'#' + relpath if relpath else ''}"
+
+
+def write_layers_spdx(d):
+    d.setVar("PN", "layers")
+    d.setVar("BB_TASKHASH", "layers")
+    oe.spdx_common.load_spdx_license_data(d)
+    deploy_dir_spdx = Path(d.getVar("DEPLOY_DIR_SPDX"))
+
+    layer_objset = oe.sbom30.ObjectSet.new_objset(d, "layers")
+
+    layers = oe.buildcfg.get_layer_revisions(d)
+    for (l_path, l_name, l_branch, l_rev, l_ismodified) in layers:
+        downloadLocation = get_layer_downloadLocation(d, l_path, l_name, l_branch, l_rev, l_ismodified)
+
+
+        if oe.utils.vartrue("SPDX_REQUIRE_LAYER_ASSERTION", True, False, d) and downloadLocation == "NOASSERTION":
+            bb.fatal(f"Layer {l_name} does not have a valid downloadLocation")
+        layer = oe.spdx30.software_Package(
+            _id=layer_objset.new_spdxid("layer", l_name),
+            creationInfo=layer_objset.doc.creationInfo,
+            name=l_name,
+            software_packageVersion=l_rev,
+            software_primaryPurpose=oe.spdx30.software_SoftwarePurpose.specification,
+            software_downloadLocation=downloadLocation,
+            software_sourceInfo=json.dumps(
+                {
+                    "branch": l_branch,
+                    "is-modified": (len(l_ismodified) > 0),
+                },
+                separators=(",", ":"),
+            ),
+        )
+
+        layer_objset.add_root(layer)
+        layer_objset.set_element_alias(layer)
+
+    oe.sbom30.write_jsonld_doc(d, layer_objset, deploy_dir_spdx / "layers.spdx.json")
+
+
+
 def write_bitbake_spdx(d):
     # Set PN to "bitbake" so that SPDX IDs can be generated
     d.setVar("PN", "bitbake")
