Repository: theupdateframework/python-tuf
Pull request #2557 (2024-02-16 08:49:23)

Release 3.1.1
This PR is for series/3.1 branch that I just created (off of v3.1.0 release tag).

It contains 
* backport of the fix for GHSA-77hh-43cm-v8j6
* backport python version choice when linting in CI 
* version bump
* changelog update


Patch:
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index dbeadb62..5545ce4d 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -1,5 +1,15 @@
 # Changelog
 
+## v3.1.1
+
+This is a security fix release to address advisory
+GHSA-77hh-43cm-v8j6. The issue does **not** affect tuf.ngclient
+users, but could affect tuf.api.metadata users.
+
+### Changed
+* Added additional input validation to
+  `tuf.api.metadata.Targets.get_delegated_role()`
+
 ## v3.1.0
 
 ### Added
diff --git a/tuf/__init__.py b/tuf/__init__.py
index 36723485..56299e37 100755
--- a/tuf/__init__.py
+++ b/tuf/__init__.py
@@ -5,4 +5,4 @@
 """
 
 # This value is used in the requests user agent.
-__version__ = "3.1.0"
+__version__ = "3.1.1"
diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py
index 205678a3..7af43d05 100644
--- a/tuf/api/metadata.py
+++ b/tuf/api/metadata.py
@@ -2044,10 +2044,13 @@ def get_delegated_role(self, delegated_role: str) -> Role:
         if self.delegations is None:
             raise ValueError("No delegations found")
 
+        role: Optional[Role] = None
         if self.delegations.roles is not None:
-            role: Optional[Role] = self.delegations.roles.get(delegated_role)
-        else:
-            role = self.delegations.succinct_roles
+            role = self.delegations.roles.get(delegated_role)
+        elif self.delegations.succinct_roles is not None:
+            succinct = self.delegations.succinct_roles
+            if succinct.is_delegated_role(delegated_role):
+                role = succinct
 
         if not role:
             raise ValueError(f"Delegated role {delegated_role} not found")


Test patch:
diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml
index 128fb8ad..f34f347d 100644
--- a/.github/workflows/_test.yml
+++ b/.github/workflows/_test.yml
@@ -14,10 +14,10 @@ jobs:
       - name: Checkout TUF
         uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
 
-      - name: Set up Python 3.x
+      - name: Set up Python (oldest supported version)
         uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1
         with:
-          python-version: 3.x
+          python-version: 3.8
           cache: 'pip'
           cache-dependency-path: 'requirements/*.txt'
 
diff --git a/tests/test_api.py b/tests/test_api.py
index 517ff5bd..5850f27e 100755
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -1008,6 +1008,33 @@ def test_get_roles_in_succinct_roles(self) -> None:
             expected_bin_suffix = f"{bin_numer:0{expected_suffix_length}x}"
             self.assertEqual(role_name, f"bin-{expected_bin_suffix}")
 
+    def test_delegations_get_delegated_role(self) -> None:
+        delegations = Delegations({}, {})
+        targets = Targets(delegations=delegations)
+
+        with self.assertRaises(ValueError):
+            targets.get_delegated_role("abc")
+
+        # test "normal" delegated role (path or path_hash_prefix)
+        role = DelegatedRole("delegated", [], 1, False, [])
+        delegations.roles = {"delegated": role}
+        with self.assertRaises(ValueError):
+            targets.get_delegated_role("not-delegated")
+        self.assertEqual(targets.get_delegated_role("delegated"), role)
+        delegations.roles = None
+
+        # test succinct delegation
+        bit_len = 3
+        role2 = SuccinctRoles([], 1, bit_len, "prefix")
+        delegations.succinct_roles = role2
+        for name in ["prefix-", "prefix--1", f"prefix-{2**bit_len:0x}"]:
+            with self.assertRaises(ValueError, msg=f"role name '{name}'"):
+                targets.get_delegated_role(name)
+        for i in range(0, 2**bit_len):
+            self.assertEqual(
+                targets.get_delegated_role(f"prefix-{i:0x}"), role2
+            )
+
 
 # Run unit test.
 if __name__ == "__main__":