Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-tuf for openSUSE:Factory 
checked in at 2026-10-26 10:02:49
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-tuf (Old)
 and      /work/SRC/openSUSE:Factory/.python-tuf.new.2252680 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-tuf"

Mon Oct 26 10:02:49 2026 rev:4 rq:1392537 version:7.0.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-tuf/python-tuf.changes    2026-05-20 
15:26:20.130624783 +0200
+++ /work/SRC/openSUSE:Factory/.python-tuf.new.2252680/python-tuf.changes       
2026-10-26 10:04:18.727558278 +0100
@@ -1,0 +2,11 @@
+Sun Oct 25 20:12:20 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 7.0.1:
+  * Fix hash method on Role and DelegatedRole
+  * Fix incorrect dependency range for urllib3
+  * ngclient: Fix connection exception handling
+  * ngclient: Fix incorrect delegation in diamond-shape case
+  * ngclient: Avoid use of symlink on Windows: it requires
+    privileges
+
+-------------------------------------------------------------------

Old:
----
  tuf-7.0.0.tar.gz

New:
----
  tuf-7.0.1.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-tuf.spec ++++++
--- /var/tmp/diff_new_pack.1P0FvJ/_old  2026-10-26 10:04:20.415629042 +0100
+++ /var/tmp/diff_new_pack.1P0FvJ/_new  2026-10-26 10:04:20.421629294 +0100
@@ -17,7 +17,7 @@
 
 
 Name:           python-tuf
-Version:        7.0.0
+Version:        7.0.1
 Release:        0
 Summary:        A secure updater framework for Python
 License:        MIT

++++++ tuf-7.0.0.tar.gz -> tuf-7.0.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/.github/workflows/_test.yml 
new/python-tuf-7.0.1/.github/workflows/_test.yml
--- old/python-tuf-7.0.0/.github/workflows/_test.yml    2026-05-18 
09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/.github/workflows/_test.yml    2026-09-01 
13:32:56.000000000 +0200
@@ -11,12 +11,12 @@
 
     steps:
       - name: Checkout TUF
-        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 
v6.0.2
+        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # 
v7.0.1
         with:
           persist-credentials: false
 
       - name: Set up Python (oldest supported version)
-        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # 
v6.2.0
+        uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # 
v7.0.0
         with:
           python-version: "3.10"
           cache: 'pip'
@@ -50,12 +50,12 @@
 
     steps:
       - name: Checkout TUF
-        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 
v6.0.2
+        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # 
v7.0.1
         with:
           persist-credentials: false
 
       - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # 
v6.2.0
+        uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # 
v7.0.0
         with:
           python-version: ${{ matrix.python-version }}
           cache: 'pip'
@@ -67,9 +67,38 @@
         run: |
           python3 -m pip install --constraint requirements/build.txt tox 
coveralls
 
-      - name: Run tox
+      - name: Run tox (non-Windows)
+        if: runner.os != 'Windows'
         run: tox -e py
 
+      - name: Run tox on Windows (unprivileged user without Developer Mode)
+        if: runner.os == 'Windows'
+        shell: powershell
+        run: |
+          # Disable Developer Mode in registry
+          Set-ItemProperty -Path 
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" -Name 
"AllowDevelopmentWithoutDevLicense" -Value 0 -ErrorAction SilentlyContinue
+
+          # Create a non-admin local user
+          $secpasswd = ConvertTo-SecureString "Password123!" -AsPlainText 
-Force
+          $user = New-LocalUser -Name "testuser" -Password $secpasswd 
-FullName "Test User" -ErrorAction SilentlyContinue
+
+          # Grant testuser permission to workspace directory and ancestors
+          $curr = Get-Item $pwd
+          while ($curr -ne $null) {
+              $acl = Get-Acl $curr.FullName
+              $rule = New-Object 
System.Security.AccessControl.FileSystemAccessRule("testuser", "FullControl", 
"ContainerInherit,ObjectInherit", "None", "Allow")
+              $acl.AddAccessRule($rule)
+              Set-Acl $curr.FullName $acl
+              $curr = $curr.Parent
+          }
+
+          # Run tox as unprivileged user
+          $cred = New-Object System.Management.Automation.PSCredential 
("testuser", $secpasswd)
+          $process = Start-Process -FilePath "tox" -ArgumentList "-e py" 
-Credential $cred -NoNewWindow -Wait -PassThru
+          if ($process.ExitCode -ne 0) {
+              exit $process.ExitCode
+          }
+
       - name: Publish on coveralls.io
         # A failure to publish coverage results on coveralls should not
         # be a reason for a job failure.
@@ -99,7 +128,7 @@
         run: touch requirements.txt
 
       - name: Set up Python
-        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # 
v6.2.0
+        uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # 
v7.0.0
         with:
           python-version: '3.x'
           cache: 'pip'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-tuf-7.0.0/.github/workflows/_test_sslib_main.yml 
new/python-tuf-7.0.1/.github/workflows/_test_sslib_main.yml
--- old/python-tuf-7.0.0/.github/workflows/_test_sslib_main.yml 2026-05-18 
09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/.github/workflows/_test_sslib_main.yml 2026-09-01 
13:32:56.000000000 +0200
@@ -11,12 +11,12 @@
 
     steps:
       - name: Checkout TUF
-        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 
v6.0.2
+        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # 
v7.0.1
         with:
           persist-credentials: false
 
       - name: Set up Python
-        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # 
v6.2.0
+        uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # 
v7.0.0
         with:
           python-version: '3.x'
           cache: 'pip'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/.github/workflows/cd.yml 
new/python-tuf-7.0.1/.github/workflows/cd.yml
--- old/python-tuf-7.0.0/.github/workflows/cd.yml       2026-05-18 
09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/.github/workflows/cd.yml       2026-09-01 
13:32:56.000000000 +0200
@@ -18,13 +18,13 @@
     needs: test
     steps:
       - name: Checkout release tag
-        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 
v6.0.2
+        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # 
v7.0.1
         with:
           persist-credentials: false
           ref: ${{ github.event.workflow_run.head_branch }}
 
       - name: Set up Python
-        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # 
v6.2.0
+        uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # 
v7.0.0
         with:
           python-version: '3.x'
 
@@ -103,7 +103,7 @@
       - name: Publish binary wheel and source tarball on PyPI
         # Only attempt pypi upload in upstream repository
         if: github.repository == 'theupdateframework/python-tuf'
-        uses: 
pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
+        uses: 
pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
 
       - name: Finalize GitHub release
         uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # 
v9.0.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-tuf-7.0.0/.github/workflows/codeql-analysis.yml 
new/python-tuf-7.0.1/.github/workflows/codeql-analysis.yml
--- old/python-tuf-7.0.0/.github/workflows/codeql-analysis.yml  2026-05-18 
09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/.github/workflows/codeql-analysis.yml  2026-09-01 
13:32:56.000000000 +0200
@@ -23,14 +23,14 @@
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+      uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
       with:
         persist-credentials: false
 
     - name: Initialize CodeQL
-      uses: github/codeql-action/[email protected] # zizmor: ignore[unpinned-uses]
+      uses: github/codeql-action/[email protected] # zizmor: ignore[unpinned-uses]
       with:
         languages: 'python'
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/[email protected] # zizmor: 
ignore[unpinned-uses]
+      uses: github/codeql-action/[email protected] # zizmor: 
ignore[unpinned-uses]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/.github/workflows/conformance.yml 
new/python-tuf-7.0.1/.github/workflows/conformance.yml
--- old/python-tuf-7.0.0/.github/workflows/conformance.yml      2026-05-18 
09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/.github/workflows/conformance.yml      2026-09-01 
13:32:56.000000000 +0200
@@ -16,7 +16,7 @@
     runs-on: ubuntu-latest
     steps:
       - name: Checkout conformance client
-        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 
v6.0.2
+        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # 
v7.0.1
         with:
           persist-credentials: false
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-tuf-7.0.0/.github/workflows/dependency-review.yml 
new/python-tuf-7.0.1/.github/workflows/dependency-review.yml
--- old/python-tuf-7.0.0/.github/workflows/dependency-review.yml        
2026-05-18 09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/.github/workflows/dependency-review.yml        
2026-09-01 13:32:56.000000000 +0200
@@ -16,8 +16,8 @@
     runs-on: ubuntu-latest
     steps:
       - name: 'Checkout Repository'
-        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 
v6.0.2
+        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # 
v7.0.1
         with:
           persist-credentials: false
       - name: 'Dependency Review'
-        uses: actions/[email protected] # zizmor: 
ignore[unpinned-uses]
\ No newline at end of file
+        uses: actions/[email protected] # zizmor: 
ignore[unpinned-uses]
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/.github/workflows/scorecards.yml 
new/python-tuf-7.0.1/.github/workflows/scorecards.yml
--- old/python-tuf-7.0.0/.github/workflows/scorecards.yml       2026-05-18 
09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/.github/workflows/scorecards.yml       2026-09-01 
13:32:56.000000000 +0200
@@ -22,12 +22,12 @@
 
     steps:
       - name: "Checkout code"
-        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 
v6.0.2
+        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # 
v7.0.1
         with:
           persist-credentials: false
 
       - name: "Run analysis"
-        uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # 
v2.4.3
+        uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # 
v2.4.4
         with:
           results_file: results.sarif
           # sarif format required by upload-sarif action
@@ -37,6 +37,6 @@
           publish_results: true
 
       - name: "Upload to code-scanning dashboard"
-        uses: github/codeql-action/[email protected] # zizmor: 
ignore[unpinned-uses]
+        uses: github/codeql-action/[email protected] # zizmor: 
ignore[unpinned-uses]
         with:
           sarif_file: results.sarif
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-tuf-7.0.0/.github/workflows/specification-version-check.yml 
new/python-tuf-7.0.1/.github/workflows/specification-version-check.yml
--- old/python-tuf-7.0.0/.github/workflows/specification-version-check.yml      
2026-05-18 09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/.github/workflows/specification-version-check.yml      
2026-09-01 13:32:56.000000000 +0200
@@ -14,10 +14,10 @@
     outputs:
       version: ${{ steps.get-version.outputs.version }}
     steps:
-      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 
v6.0.2
+      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # 
v7.0.1
         with:
          persist-credentials: false
-      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # 
v6.2.0
+      - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # 
v7.0.0
         with:
           python-version: "3.x"
       - id: get-version
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/docs/CHANGELOG.md 
new/python-tuf-7.0.1/docs/CHANGELOG.md
--- old/python-tuf-7.0.0/docs/CHANGELOG.md      2026-05-18 09:58:45.000000000 
+0200
+++ new/python-tuf-7.0.1/docs/CHANGELOG.md      2026-09-01 13:32:56.000000000 
+0200
@@ -2,6 +2,16 @@
 
 ## Unreleased
 
+## v7.0.1
+
+### Fixed
+
+* Fix hash method on Role and DelegatedRole (#2973)
+* Fix incorrect dependency range for urllib3 (#2990)
+* ngclient: Fix connection exception handling (#2964, #2993)
+* ngclient: Fix incorrect delegation in diamond-shape case (#2978)
+* ngclient: Avoid use of symlink on Windows: it requires privileges (#2981)
+
 ## v7.0.0
 
 This is a major release only because of a minor ngclient API tweak: there
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/docs/INSTALLATION.rst 
new/python-tuf-7.0.1/docs/INSTALLATION.rst
--- old/python-tuf-7.0.0/docs/INSTALLATION.rst  2026-05-18 09:58:45.000000000 
+0200
+++ new/python-tuf-7.0.1/docs/INSTALLATION.rst  2026-09-01 13:32:56.000000000 
+0200
@@ -5,6 +5,11 @@
 `PyPI <https://pypi.org/project/tuf/>`_ with
 `pip <https://pip.pypa.io/en/stable/>`_.
 
+.. note::
+
+   Windows users: replace ``python3`` with ``python`` or use the
+   ``py`` launcher (e.g. ``py -m pip install tuf``).
+
 ::
 
    python3 -m pip install tuf
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/examples/client/README.md 
new/python-tuf-7.0.1/examples/client/README.md
--- old/python-tuf-7.0.0/examples/client/README.md      2026-05-18 
09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/examples/client/README.md      2026-09-01 
13:32:56.000000000 +0200
@@ -42,5 +42,5 @@
 ./client --url https://jku.github.io/tuf-demo tofu
 
 # Then download example files from the repository:
-./client --url https://jku.github.io/tuf-demo download 
demo/succinctly-delegated-1.txt
+./client --url https://jku.github.io/tuf-demo download 
rdimitrov/artifact-example.md
 ```
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/examples/manual_repo/basic_repo.py 
new/python-tuf-7.0.1/examples/manual_repo/basic_repo.py
--- old/python-tuf-7.0.0/examples/manual_repo/basic_repo.py     2026-05-18 
09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/examples/manual_repo/basic_repo.py     2026-09-01 
13:32:56.000000000 +0200
@@ -1,3 +1,6 @@
+# Copyright 2021-2024, New York University and the TUF contributors
+# SPDX-License-Identifier: MIT OR Apache-2.0
+
 """
 A TUF repository example using the low-level TUF Metadata API.
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-tuf-7.0.0/examples/manual_repo/hashed_bin_delegation.py 
new/python-tuf-7.0.1/examples/manual_repo/hashed_bin_delegation.py
--- old/python-tuf-7.0.0/examples/manual_repo/hashed_bin_delegation.py  
2026-05-18 09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/examples/manual_repo/hashed_bin_delegation.py  
2026-09-01 13:32:56.000000000 +0200
@@ -1,3 +1,6 @@
+# Copyright 2021-2025, New York University and the TUF contributors
+# SPDX-License-Identifier: MIT OR Apache-2.0
+
 """
 A TUF hash bin delegation example using the low-level TUF Metadata API.
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-tuf-7.0.0/examples/manual_repo/succinct_hash_bin_delegations.py 
new/python-tuf-7.0.1/examples/manual_repo/succinct_hash_bin_delegations.py
--- old/python-tuf-7.0.0/examples/manual_repo/succinct_hash_bin_delegations.py  
2026-05-18 09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/examples/manual_repo/succinct_hash_bin_delegations.py  
2026-09-01 13:32:56.000000000 +0200
@@ -1,4 +1,4 @@
-# Copyright New York University and the TUF contributors
+# Copyright 2022-2024, New York University and the TUF contributors
 # SPDX-License-Identifier: MIT OR Apache-2.0
 """
 A TUF succinct hash bin delegation example using the low-level TUF Metadata 
API.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/pyproject.toml 
new/python-tuf-7.0.1/pyproject.toml
--- old/python-tuf-7.0.0/pyproject.toml 2026-05-18 09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/pyproject.toml 2026-09-01 13:32:56.000000000 +0200
@@ -1,5 +1,5 @@
 [build-system]
-requires = ["hatchling==1.29.0"]
+requires = ["hatchling==1.32.0"]
 build-backend = "hatchling.build"
 
 [project]
@@ -42,7 +42,7 @@
 ]
 dependencies = [
   "securesystemslib~=1.0",
-  "urllib3<3,>=1.21.1",
+  "urllib3~=2.0",
 ]
 dynamic = ["version"]
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/requirements/build.txt 
new/python-tuf-7.0.1/requirements/build.txt
--- old/python-tuf-7.0.0/requirements/build.txt 2026-05-18 09:58:45.000000000 
+0200
+++ new/python-tuf-7.0.1/requirements/build.txt 2026-09-01 13:32:56.000000000 
+0200
@@ -1,4 +1,4 @@
 # The build and tox versions specified here are also used as constraints
 # during CI and CD Github workflows
-build==1.4.4
+build==1.5.0
 tox==4.1.2
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/requirements/lint.txt 
new/python-tuf-7.0.1/requirements/lint.txt
--- old/python-tuf-7.0.0/requirements/lint.txt  2026-05-18 09:58:45.000000000 
+0200
+++ new/python-tuf-7.0.1/requirements/lint.txt  2026-09-01 13:32:56.000000000 
+0200
@@ -6,9 +6,9 @@
 # Lint tools
 # (We are not so interested in the specific versions of the tools: the versions
 # are pinned to prevent unexpected linting failures when tools update)
-ruff==0.15.12
-mypy==1.20.2
-zizmor==1.24.1
+ruff==0.16.4
+mypy==2.3.1
+zizmor==1.29.0
 
 # Required for type stubs
 freezegun==1.5.5
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/requirements/main.txt 
new/python-tuf-7.0.1/requirements/main.txt
--- old/python-tuf-7.0.0/requirements/main.txt  2026-05-18 09:58:45.000000000 
+0200
+++ new/python-tuf-7.0.1/requirements/main.txt  2026-09-01 13:32:56.000000000 
+0200
@@ -7,4 +7,4 @@
 # triggers CI/CD builds to automatically test against updated dependencies.
 #
 securesystemslib[crypto]
-urllib3
+urllib3~=2.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/requirements/pinned.txt 
new/python-tuf-7.0.1/requirements/pinned.txt
--- old/python-tuf-7.0.0/requirements/pinned.txt        2026-05-18 
09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/requirements/pinned.txt        2026-09-01 
13:32:56.000000000 +0200
@@ -4,13 +4,13 @@
 #
 #    pip-compile --output-file=requirements/pinned.txt --strip-extras 
requirements/main.txt
 #
-cffi==2.0.0
+cffi==2.1.1
     # via cryptography
-cryptography==47.0.0
+cryptography==50.0.0
     # via securesystemslib
 pycparser==3.0
     # via cffi
-securesystemslib==1.3.1
+securesystemslib==1.5.0
     # via -r requirements/main.txt
-urllib3==2.6.3
+urllib3==2.7.0
     # via -r requirements/main.txt
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/requirements/test.txt 
new/python-tuf-7.0.1/requirements/test.txt
--- old/python-tuf-7.0.0/requirements/test.txt  2026-05-18 09:58:45.000000000 
+0200
+++ new/python-tuf-7.0.1/requirements/test.txt  2026-09-01 13:32:56.000000000 
+0200
@@ -4,5 +4,5 @@
 -r pinned.txt
 
 # coverage measurement
-coverage[toml]==7.13.5
+coverage[toml]==7.15.4
 freezegun==1.5.5
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/tests/__init__.py 
new/python-tuf-7.0.1/tests/__init__.py
--- old/python-tuf-7.0.0/tests/__init__.py      2026-05-18 09:58:45.000000000 
+0200
+++ new/python-tuf-7.0.1/tests/__init__.py      2026-09-01 13:32:56.000000000 
+0200
@@ -0,0 +1,2 @@
+# Copyright 2013-2024, the TUF contributors
+# SPDX-License-Identifier: MIT OR Apache-2.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/tests/generated_data/__init__.py 
new/python-tuf-7.0.1/tests/generated_data/__init__.py
--- old/python-tuf-7.0.0/tests/generated_data/__init__.py       2026-05-18 
09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/tests/generated_data/__init__.py       2026-09-01 
13:32:56.000000000 +0200
@@ -0,0 +1,2 @@
+# Copyright 2013-2022, New York University and the TUF contributors
+# SPDX-License-Identifier: MIT OR Apache-2.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/tests/generated_data/generate_md.py 
new/python-tuf-7.0.1/tests/generated_data/generate_md.py
--- old/python-tuf-7.0.0/tests/generated_data/generate_md.py    2026-05-18 
09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/tests/generated_data/generate_md.py    2026-09-01 
13:32:56.000000000 +0200
@@ -1,8 +1,8 @@
-"""Script for generating new metadata files."""
-
-# Copyright New York University and the TUF contributors
+# Copyright 2022-2024, New York University and the TUF contributors
 # SPDX-License-Identifier: MIT OR Apache-2.0
 
+"""Script for generating new metadata files."""
+
 from __future__ import annotations
 
 import os
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/tests/test_api.py 
new/python-tuf-7.0.1/tests/test_api.py
--- old/python-tuf-7.0.0/tests/test_api.py      2026-05-18 09:58:45.000000000 
+0200
+++ new/python-tuf-7.0.1/tests/test_api.py      2026-09-01 13:32:56.000000000 
+0200
@@ -34,6 +34,7 @@
     Delegations,
     Metadata,
     MetaFile,
+    Role,
     Root,
     RootVerificationResult,
     Signature,
@@ -1082,6 +1083,34 @@
             self.assertFalse(role.is_delegated_path("a/non-matching path"))
             self.assertTrue(role.is_delegated_path("a/path"))
 
+    def test_role_and_delegated_role_hash(self) -> None:
+        # Role.__hash__ previously tried to hash self.keyids (a list) and
+        # self.unrecognized_fields (a dict), both unhashable, so hash()
+        # crashed with TypeError for every Role subclass.
+        role = Role(["keyid1", "keyid2"], 1)
+        self.assertIsInstance(hash(role), int)
+
+        # equal objects must produce equal hashes (Python data model)
+        role2 = Role(["keyid1", "keyid2"], 1)
+        self.assertEqual(role, role2)
+        self.assertEqual(hash(role), hash(role2))
+
+        # DelegatedRole.__hash__ also referenced a non-existent 'path'
+        # attribute (the real attribute is 'paths'); verify it's hashable
+        # with both paths and path_hash_prefixes variants.
+        dr = DelegatedRole("role1", [], 1, False, ["*"], None)
+        self.assertIsInstance(hash(dr), int)
+        dr2 = DelegatedRole("role1", [], 1, False, ["*"], None)
+        self.assertEqual(dr, dr2)
+        self.assertEqual(hash(dr), hash(dr2))
+
+        dr_prefix = DelegatedRole("role2", [], 1, False, None, ["abc"])
+        self.assertIsInstance(hash(dr_prefix), int)
+
+        # a DelegatedRole must now actually work as a set member / dict key
+        role_set = {dr, dr2}
+        self.assertEqual(len(role_set), 1)
+
     def test_is_delegated_role_in_succinct_roles(self) -> None:
         succinct_roles = SuccinctRoles([], 1, 5, "bin")
         false_role_name_examples = [
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/tests/test_fetcher_ng.py 
new/python-tuf-7.0.1/tests/test_fetcher_ng.py
--- old/python-tuf-7.0.0/tests/test_fetcher_ng.py       2026-05-18 
09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/tests/test_fetcher_ng.py       2026-09-01 
13:32:56.000000000 +0200
@@ -122,6 +122,46 @@
             next(self.fetcher.fetch(self.url))
         mock_response.stream.assert_called_once()
 
+    # urllib3 raises ReadTimeoutError directly when the gap timeout expires
+    # mid-stream: it is a TimeoutError but not a MaxRetryError, so it is not
+    # covered by the MaxRetryError case above.
+    @patch.object(urllib3.PoolManager, "request")
+    def test_response_read_timeout_error(self, mock_session_get: Mock) -> None:
+        mock_response = Mock()
+        mock_response.status = 200
+        attr = {
+            "stream.side_effect": urllib3.exceptions.ReadTimeoutError(
+                urllib3.connectionpool.ConnectionPool("localhost"),
+                "",
+                "Read timed out.",
+            )
+        }
+        mock_response.configure_mock(**attr)
+        mock_session_get.return_value = mock_response
+
+        with self.assertRaises(exceptions.SlowRetrievalError):
+            next(self.fetcher.fetch(self.url))
+        mock_response.stream.assert_called_once()
+
+    # The public download_* API documents DownloadError: a mid-stream timeout
+    # must not escape as a raw urllib3 error.
+    @patch.object(urllib3.PoolManager, "request")
+    def test_download_bytes_read_timeout(self, mock_session_get: Mock) -> None:
+        mock_response = Mock()
+        mock_response.status = 200
+        attr = {
+            "stream.side_effect": urllib3.exceptions.ReadTimeoutError(
+                urllib3.connectionpool.ConnectionPool("localhost"),
+                "",
+                "Read timed out.",
+            )
+        }
+        mock_response.configure_mock(**attr)
+        mock_session_get.return_value = mock_response
+
+        with self.assertRaises(exceptions.SlowRetrievalError):
+            self.fetcher.download_bytes(self.url, self.file_length)
+
     # Read/connect session timeout error
     @patch.object(
         urllib3.PoolManager,
@@ -137,6 +177,25 @@
             self.fetcher.fetch(self.url)
         mock_session_get.assert_called_once()
 
+    # A non-timeout connection failure (e.g. TLS error) must surface as the
+    # original error, not be masked by an UnboundLocalError on "response".
+    @patch.object(
+        urllib3.PoolManager,
+        "request",
+        side_effect=urllib3.exceptions.MaxRetryError(
+            urllib3.connectionpool.ConnectionPool("localhost"),
+            "",
+            urllib3.exceptions.SSLError("certificate verify failed"),
+        ),
+    )
+    def test_session_get_ssl_error(self, mock_session_get: Mock) -> None:
+        with self.assertRaises(exceptions.DownloadError) as cm:
+            self.fetcher.fetch(self.url)
+        mock_session_get.assert_called_once()
+        self.assertIsInstance(
+            cm.exception.__cause__, urllib3.exceptions.MaxRetryError
+        )
+
     # Simple bytes download
     def test_download_bytes(self) -> None:
         data = self.fetcher.download_bytes(self.url, self.file_length)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/tests/test_metadata_eq_.py 
new/python-tuf-7.0.1/tests/test_metadata_eq_.py
--- old/python-tuf-7.0.0/tests/test_metadata_eq_.py     2026-05-18 
09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/tests/test_metadata_eq_.py     2026-09-01 
13:32:56.000000000 +0200
@@ -1,4 +1,4 @@
-# Copyright New York University and the TUF contributors
+# Copyright 2022-2025, New York University and the TUF contributors
 # SPDX-License-Identifier: MIT OR Apache-2.0
 
 """Test __eq__ implementations of classes inside tuf/api/metadata.py."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/tests/test_metadata_generation.py 
new/python-tuf-7.0.1/tests/test_metadata_generation.py
--- old/python-tuf-7.0.0/tests/test_metadata_generation.py      2026-05-18 
09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/tests/test_metadata_generation.py      2026-09-01 
13:32:56.000000000 +0200
@@ -1,8 +1,8 @@
-"""Unit tests for 'tests/generated_data/generate_md.py'."""
-
-# Copyright New York University and the TUF contributors
+# Copyright 2022-2024, New York University and the TUF contributors
 # SPDX-License-Identifier: MIT OR Apache-2.0
 
+"""Unit tests for 'tests/generated_data/generate_md.py'."""
+
 import sys
 import unittest
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-tuf-7.0.0/tests/test_metadata_serialization.py 
new/python-tuf-7.0.1/tests/test_metadata_serialization.py
--- old/python-tuf-7.0.0/tests/test_metadata_serialization.py   2026-05-18 
09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/tests/test_metadata_serialization.py   2026-09-01 
13:32:56.000000000 +0200
@@ -1,4 +1,4 @@
-# Copyright New York University and the TUF contributors
+# Copyright 2021-2024, New York University and the TUF contributors
 # SPDX-License-Identifier: MIT OR Apache-2.0
 
 """Unit tests testing tuf/api/metadata.py classes
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/tests/test_trusted_metadata_set.py 
new/python-tuf-7.0.1/tests/test_trusted_metadata_set.py
--- old/python-tuf-7.0.0/tests/test_trusted_metadata_set.py     2026-05-18 
09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/tests/test_trusted_metadata_set.py     2026-09-01 
13:32:56.000000000 +0200
@@ -1,3 +1,6 @@
+# Copyright 2021-2026, the TUF contributors
+# SPDX-License-Identifier: MIT OR Apache-2.0
+
 """Unit tests for 'tuf/ngclient/_internal/trusted_metadata_set.py'."""
 
 from __future__ import annotations
@@ -149,6 +152,19 @@
 
         self.assertTrue(count, 6)
 
+    def test_contains(self) -> None:
+        self.assertFalse(self.trusted_set.contains("role1", Targets.type))
+
+        self.trusted_set.update_timestamp(self.metadata[Timestamp.type])
+        self.trusted_set.update_snapshot(self.metadata[Snapshot.type])
+        self.trusted_set.update_targets(self.metadata[Targets.type])
+        self.trusted_set.update_delegated_targets(
+            self.metadata["role1"], "role1", Targets.type
+        )
+
+        self.assertTrue(self.trusted_set.contains("role1", Targets.type))
+        self.assertFalse(self.trusted_set.contains("role1", "other-parent"))
+
     def test_update_metadata_output(self) -> None:
         timestamp = self.trusted_set.update_timestamp(
             self.metadata["timestamp"]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-tuf-7.0.0/tests/test_updater_consistent_snapshot.py 
new/python-tuf-7.0.1/tests/test_updater_consistent_snapshot.py
--- old/python-tuf-7.0.0/tests/test_updater_consistent_snapshot.py      
2026-05-18 09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/tests/test_updater_consistent_snapshot.py      
2026-09-01 13:32:56.000000000 +0200
@@ -126,23 +126,20 @@
     ) -> None:
         # Test if the client fetches and stores metadata files with the
         # correct version prefix, depending on 'consistent_snapshot' config
-        try:
-            consistent_snapshot: bool = test_case_data["consistent_snapshot"]
-            exp_calls: list[Any] = test_case_data["calls"]
-
-            self.setup_subtest(consistent_snapshot)
-            updater = self._init_updater()
-
-            # cleanup fetch tracker metadata
-            self.sim.fetch_tracker.metadata.clear()
-            updater.refresh()
-
-            # metadata files are fetched with the expected version (or None)
-            self.assertListEqual(self.sim.fetch_tracker.metadata, exp_calls)
-            # metadata files are always persisted without a version prefix
-            self._assert_metadata_files_exist(TOP_LEVEL_ROLE_NAMES)
-        finally:
-            self.teardown_subtest()
+        consistent_snapshot: bool = test_case_data["consistent_snapshot"]
+        exp_calls: list[Any] = test_case_data["calls"]
+
+        self.setup_subtest(consistent_snapshot)
+        updater = self._init_updater()
+
+        # cleanup fetch tracker metadata
+        self.sim.fetch_tracker.metadata.clear()
+        updater.refresh()
+
+        # metadata files are fetched with the expected version (or None)
+        self.assertListEqual(self.sim.fetch_tracker.metadata, exp_calls)
+        # metadata files are always persisted without a version prefix
+        self._assert_metadata_files_exist(TOP_LEVEL_ROLE_NAMES)
 
     delegated_roles_data = {
         "consistent_snaphot disabled": {
@@ -161,35 +158,30 @@
     ) -> None:
         # Test if the client fetches and stores delegated metadata files with
         # the correct version prefix, depending on 'consistent_snapshot' config
-        try:
-            consistent_snapshot: bool = test_case_data["consistent_snapshot"]
-            exp_version: int | None = test_case_data["expected_version"]
-            rolenames = ["role1", "..", "."]
-            exp_calls = [(role, exp_version) for role in rolenames]
-
-            self.setup_subtest(consistent_snapshot)
-            # Add new delegated targets
-            spec_version = ".".join(SPECIFICATION_VERSION)
-            for role in rolenames:
-                delegated_role = DelegatedRole(role, [], 1, False, ["*"], None)
-                targets = Targets(
-                    1, spec_version, self.sim.safe_expiry, {}, None
-                )
-                self.sim.add_delegation("targets", delegated_role, targets)
-            self.sim.update_snapshot()
-            updater = self._init_updater()
-            updater.refresh()
-
-            # cleanup fetch tracker metadata
-            self.sim.fetch_tracker.metadata.clear()
-            # trigger updater to fetch the delegated metadata
-            updater.get_targetinfo("anything")
-            # metadata files are fetched with the expected version (or None)
-            self.assertListEqual(self.sim.fetch_tracker.metadata, exp_calls)
-            # metadata files are always persisted without a version prefix
-            self._assert_metadata_files_exist(rolenames)
-        finally:
-            self.teardown_subtest()
+        consistent_snapshot: bool = test_case_data["consistent_snapshot"]
+        exp_version: int | None = test_case_data["expected_version"]
+        rolenames = ["role1", "..", "."]
+        exp_calls = [(role, exp_version) for role in rolenames]
+
+        self.setup_subtest(consistent_snapshot)
+        # Add new delegated targets
+        spec_version = ".".join(SPECIFICATION_VERSION)
+        for role in rolenames:
+            delegated_role = DelegatedRole(role, [], 1, False, ["*"], None)
+            targets = Targets(1, spec_version, self.sim.safe_expiry, {}, None)
+            self.sim.add_delegation("targets", delegated_role, targets)
+        self.sim.update_snapshot()
+        updater = self._init_updater()
+        updater.refresh()
+
+        # cleanup fetch tracker metadata
+        self.sim.fetch_tracker.metadata.clear()
+        # trigger updater to fetch the delegated metadata
+        updater.get_targetinfo("anything")
+        # metadata files are fetched with the expected version (or None)
+        self.assertListEqual(self.sim.fetch_tracker.metadata, exp_calls)
+        # metadata files are always persisted without a version prefix
+        self._assert_metadata_files_exist(rolenames)
 
     targets_download_data = {
         "consistent_snaphot disabled": {
@@ -217,40 +209,37 @@
         # Test if the client fetches and stores target files with
         # the correct hash prefix, depending on 'consistent_snapshot'
         # and 'prefix_targets_with_hash' config
-        try:
-            consistent_snapshot: bool = test_case_data["consistent_snapshot"]
-            prefix_targets_with_hash: bool = test_case_data["prefix_targets"]
-            hash_algo: str | None = test_case_data["hash_algo"]
-            targetpaths: list[str] = test_case_data["targetpaths"]
-
-            self.setup_subtest(consistent_snapshot, prefix_targets_with_hash)
-            # Add targets to repository
-            for targetpath in targetpaths:
-                self.sim.targets.version += 1
-                self.sim.add_target("targets", b"content", targetpath)
-            self.sim.update_snapshot()
-
-            updater = self._init_updater()
-            updater.config.prefix_targets_with_hash = prefix_targets_with_hash
-            updater.refresh()
-
-            for path in targetpaths:
-                info = updater.get_targetinfo(path)
-                assert isinstance(info, TargetFile)
-                updater.download_target(info)
-
-                # target files are always persisted without hash prefix
-                self._assert_targets_files_exist([info.path])
-
-                # files are fetched with the expected hash prefix (or None)
-                exp_calls = [
-                    (path, None if not hash_algo else info.hashes[hash_algo])
-                ]
-
-                self.assertListEqual(self.sim.fetch_tracker.targets, exp_calls)
-                self.sim.fetch_tracker.targets.clear()
-        finally:
-            self.teardown_subtest()
+        consistent_snapshot: bool = test_case_data["consistent_snapshot"]
+        prefix_targets_with_hash: bool = test_case_data["prefix_targets"]
+        hash_algo: str | None = test_case_data["hash_algo"]
+        targetpaths: list[str] = test_case_data["targetpaths"]
+
+        self.setup_subtest(consistent_snapshot, prefix_targets_with_hash)
+        # Add targets to repository
+        for targetpath in targetpaths:
+            self.sim.targets.version += 1
+            self.sim.add_target("targets", b"content", targetpath)
+        self.sim.update_snapshot()
+
+        updater = self._init_updater()
+        updater.config.prefix_targets_with_hash = prefix_targets_with_hash
+        updater.refresh()
+
+        for path in targetpaths:
+            info = updater.get_targetinfo(path)
+            assert isinstance(info, TargetFile)
+            updater.download_target(info)
+
+            # target files are always persisted without hash prefix
+            self._assert_targets_files_exist([info.path])
+
+            # files are fetched with the expected hash prefix (or None)
+            exp_calls = [
+                (path, None if not hash_algo else info.hashes[hash_algo])
+            ]
+
+            self.assertListEqual(self.sim.fetch_tracker.targets, exp_calls)
+            self.sim.fetch_tracker.targets.clear()
 
 
 if __name__ == "__main__":
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-tuf-7.0.0/tests/test_updater_delegation_graphs.py 
new/python-tuf-7.0.1/tests/test_updater_delegation_graphs.py
--- old/python-tuf-7.0.0/tests/test_updater_delegation_graphs.py        
2026-05-18 09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/tests/test_updater_delegation_graphs.py        
2026-09-01 13:32:56.000000000 +0200
@@ -264,32 +264,29 @@
         """Test that delegated roles are traversed in the order of appearance
         in the delegator's metadata, using pre-order depth-first search"""
 
-        try:
-            exp_files = [*TOP_LEVEL_ROLE_NAMES, *test_data.visited_order]
-            exp_calls = [(role, 1) for role in test_data.visited_order]
-
-            self._init_repo(test_data)
-            self.setup_subtest()
-
-            updater = self._init_updater()
-            # restrict the max number of delegations to simplify the test
-            updater.config.max_delegations = 4
-            # Call explicitly refresh to simplify the expected_calls list
-            updater.refresh()
-            self.sim.fetch_tracker.metadata.clear()
-            # Check that metadata dir contains only top-level roles
-            self._assert_files_exist(TOP_LEVEL_ROLE_NAMES)
-
-            # Looking for a non-existing targetpath forces updater
-            # to visit all possible delegated roles
-            targetfile = updater.get_targetinfo("missingpath")
-            self.assertIsNone(targetfile)
-            # Check that the delegated roles were visited in the expected
-            # order and the corresponding metadata files were persisted
-            self.assertListEqual(self.sim.fetch_tracker.metadata, exp_calls)
-            self._assert_files_exist(exp_files)
-        finally:
-            self.teardown_subtest()
+        exp_files = [*TOP_LEVEL_ROLE_NAMES, *test_data.visited_order]
+        exp_calls = [(role, 1) for role in test_data.visited_order]
+
+        self._init_repo(test_data)
+        self.setup_subtest()
+
+        updater = self._init_updater()
+        # restrict the max number of delegations to simplify the test
+        updater.config.max_delegations = 4
+        # Call explicitly refresh to simplify the expected_calls list
+        updater.refresh()
+        self.sim.fetch_tracker.metadata.clear()
+        # Check that metadata dir contains only top-level roles
+        self._assert_files_exist(TOP_LEVEL_ROLE_NAMES)
+
+        # Looking for a non-existing targetpath forces updater
+        # to visit all possible delegated roles
+        targetfile = updater.get_targetinfo("missingpath")
+        self.assertIsNone(targetfile)
+        # Check that the delegated roles were visited in the expected
+        # order and the corresponding metadata files were persisted
+        self.assertListEqual(self.sim.fetch_tracker.metadata, exp_calls)
+        self._assert_files_exist(exp_files)
 
     invalid_metadata = {
         "unsigned delegated role": DelegationsTestCase(
@@ -305,30 +302,73 @@
 
     @utils.run_sub_tests_with_dataset(invalid_metadata)
     def test_invalid_metadata(self, test_data: DelegationsTestCase) -> None:
-        try:
-            self._init_repo(test_data)
-            # The invalid role is the last visited
-            invalid_role = test_data.visited_order[-1]
-            self.sim.signers[invalid_role].clear()
-
-            self.setup_subtest()
-            # The invalid role metadata must not be persisted
-            exp_files = [*TOP_LEVEL_ROLE_NAMES, *test_data.visited_order[:-1]]
-            exp_calls = [(role, 1) for role in test_data.visited_order]
-
-            updater = self._init_updater()
-            # Call explicitly refresh to simplify the expected_calls list
-            updater.refresh()
-            self.sim.fetch_tracker.metadata.clear()
-
-            with self.assertRaises(UnsignedMetadataError):
-                updater.get_targetinfo("missingpath")
-            # Check that there were no visited roles after the invalid one
-            # and only the valid metadata files were persisted
-            self.assertListEqual(self.sim.fetch_tracker.metadata, exp_calls)
-            self._assert_files_exist(exp_files)
-        finally:
-            self.teardown_subtest()
+        self._init_repo(test_data)
+        # The invalid role is the last visited
+        invalid_role = test_data.visited_order[-1]
+        self.sim.signers[invalid_role].clear()
+
+        self.setup_subtest()
+        # The invalid role metadata must not be persisted
+        exp_files = [*TOP_LEVEL_ROLE_NAMES, *test_data.visited_order[:-1]]
+        exp_calls = [(role, 1) for role in test_data.visited_order]
+
+        updater = self._init_updater()
+        # Call explicitly refresh to simplify the expected_calls list
+        updater.refresh()
+        self.sim.fetch_tracker.metadata.clear()
+
+        with self.assertRaises(UnsignedMetadataError):
+            updater.get_targetinfo("missingpath")
+        # Check that there were no visited roles after the invalid one
+        # and only the valid metadata files were persisted
+        self.assertListEqual(self.sim.fetch_tracker.metadata, exp_calls)
+        self._assert_files_exist(exp_files)
+
+    def test_shared_delegation_signature_check(self) -> None:
+        """Test that a role delegated by multiple parents is signature-checked
+        for each delegator link.
+        """
+        test_case = DelegationsTestCase(
+            delegations=[
+                TestDelegation("targets", "parent-a", paths=["path-a"]),
+                TestDelegation("targets", "parent-b", paths=["path-b"]),
+                TestDelegation("parent-a", "release", paths=["path-a"]),
+                TestDelegation("parent-b", "release", paths=["path-b"]),
+            ],
+            target_files=[
+                TestTarget("release", b"content-a", "path-a"),
+                TestTarget("release", b"malicious-content-b", "path-b"),
+            ],
+        )
+        self._init_repo(test_case)
+
+        # Client happy path: Both delegation paths work since release is signed
+        # by both parents
+        updater = self._init_updater()
+        target_a = updater.get_targetinfo("path-a")
+        self.assertIsNotNone(target_a)
+        target_b = updater.get_targetinfo("path-b")
+        self.assertIsNotNone(target_b)
+
+        # Create release v2: only signed by parent-a
+        first_keyid = next(iter(self.sim.signers["release"].keys()))
+        self.sim.signers["release"] = {
+            first_keyid: self.sim.signers["release"][first_keyid]
+        }
+        self.sim.md_delegates["release"].signed.version += 1
+        self.sim.update_snapshot()
+
+        # Client step 1: Get path-a targetinfo via parent-a -> release
+        # This primes TrustedMetadataSet to contain release v2
+        updater = self._init_updater()
+        target_a = updater.get_targetinfo("path-a")
+        self.assertIsNotNone(target_a)
+
+        # Client step 2: Get path-b targetinfo via parent-b -> release
+        # Must fail because release while loaded in TrustedMetadataSet, is not 
signed
+        # by parent-b's keys
+        with self.assertRaises(UnsignedMetadataError):
+            updater.get_targetinfo("path-b")
 
     def test_safely_encoded_rolenames(self) -> None:
         """Test that delegated roles names are safely encoded in the filenames
@@ -398,34 +438,31 @@
         in the delegator's metadata, using pre-order depth-first search and 
that
         they correctly refer to the corresponding hash bin prefixes"""
 
-        try:
-            exp_files = [*TOP_LEVEL_ROLE_NAMES, *test_data.visited_order]
-            exp_calls = [(role, 1) for role in test_data.visited_order]
-
-            self._init_repo(test_data)
-            self.setup_subtest()
-
-            updater = self._init_updater()
-            # Call explicitly refresh to simplify the expected_calls list
-            updater.refresh()
-            self.sim.fetch_tracker.metadata.clear()
-            # Check that metadata dir contains only top-level roles
-            self._assert_files_exist(TOP_LEVEL_ROLE_NAMES)
-
-            # Looking for a non-existing targetpath forces updater
-            # to visit a correspondning delegated role
-            targetfile = updater.get_targetinfo("missingpath")
-            self.assertIsNone(targetfile)
-            targetfile = updater.get_targetinfo("othermissingpath")
-            self.assertIsNone(targetfile)
-            targetfile = updater.get_targetinfo("thirdmissingpath")
-            self.assertIsNone(targetfile)
-            # Check that the delegated roles were visited in the expected
-            # order and the corresponding metadata files were persisted
-            self.assertListEqual(self.sim.fetch_tracker.metadata, exp_calls)
-            self._assert_files_exist(exp_files)
-        finally:
-            self.teardown_subtest()
+        exp_files = [*TOP_LEVEL_ROLE_NAMES, *test_data.visited_order]
+        exp_calls = [(role, 1) for role in test_data.visited_order]
+
+        self._init_repo(test_data)
+        self.setup_subtest()
+
+        updater = self._init_updater()
+        # Call explicitly refresh to simplify the expected_calls list
+        updater.refresh()
+        self.sim.fetch_tracker.metadata.clear()
+        # Check that metadata dir contains only top-level roles
+        self._assert_files_exist(TOP_LEVEL_ROLE_NAMES)
+
+        # Looking for a non-existing targetpath forces updater
+        # to visit a correspondning delegated role
+        targetfile = updater.get_targetinfo("missingpath")
+        self.assertIsNone(targetfile)
+        targetfile = updater.get_targetinfo("othermissingpath")
+        self.assertIsNone(targetfile)
+        targetfile = updater.get_targetinfo("thirdmissingpath")
+        self.assertIsNone(targetfile)
+        # Check that the delegated roles were visited in the expected
+        # order and the corresponding metadata files were persisted
+        self.assertListEqual(self.sim.fetch_tracker.metadata, exp_calls)
+        self._assert_files_exist(exp_files)
 
     @dataclass
     class SuccinctRolesTestCase:
@@ -482,35 +519,31 @@
         # successful traversal all top level metadata files plus the expected
         # bin should exist locally and only one bin must be downloaded.
 
-        try:
-            exp_files = [*TOP_LEVEL_ROLE_NAMES, test_data.expected_target_bin]
-            exp_calls = [(test_data.expected_target_bin, 1)]
-
-            self.sim = RepositorySimulator()
-            self.sim.add_succinct_roles("targets", test_data.bit_length, "bin")
-            self.sim.update_snapshot()
-
-            self.setup_subtest()
-
-            updater = self._init_updater()
-            # Call explicitly refresh to simplify the expected_calls list.
-            updater.refresh()
-            self.sim.fetch_tracker.metadata.clear()
-            # Check that metadata dir contains only top-level roles
-            self._assert_files_exist(TOP_LEVEL_ROLE_NAMES)
-
-            # Looking for a non-existing targetpath forces updater
-            # to visit a corresponding delegated role.
-            targetfile = updater.get_targetinfo(test_data.target_path)
-            self.assertIsNone(targetfile)
-
-            # Check that the delegated roles were visited in the expected
-            # order and the corresponding metadata files were persisted.
-            self.assertListEqual(self.sim.fetch_tracker.metadata, exp_calls)
-            self._assert_files_exist(exp_files)
+        exp_files = [*TOP_LEVEL_ROLE_NAMES, test_data.expected_target_bin]
+        exp_calls = [(test_data.expected_target_bin, 1)]
+
+        self.sim = RepositorySimulator()
+        self.sim.add_succinct_roles("targets", test_data.bit_length, "bin")
+        self.sim.update_snapshot()
+
+        self.setup_subtest()
 
-        finally:
-            self.teardown_subtest()
+        updater = self._init_updater()
+        # Call explicitly refresh to simplify the expected_calls list.
+        updater.refresh()
+        self.sim.fetch_tracker.metadata.clear()
+        # Check that metadata dir contains only top-level roles
+        self._assert_files_exist(TOP_LEVEL_ROLE_NAMES)
+
+        # Looking for a non-existing targetpath forces updater
+        # to visit a corresponding delegated role.
+        targetfile = updater.get_targetinfo(test_data.target_path)
+        self.assertIsNone(targetfile)
+
+        # Check that the delegated roles were visited in the expected
+        # order and the corresponding metadata files were persisted.
+        self.assertListEqual(self.sim.fetch_tracker.metadata, exp_calls)
+        self._assert_files_exist(exp_files)
 
 
 class TestTargetFileSearch(TestDelegations):
@@ -564,29 +597,26 @@
 
     @utils.run_sub_tests_with_dataset(targets)
     def test_targetfile_search(self, test_data: TargetTestCase) -> None:
-        try:
-            self.setup_subtest()
-            exp_files = [*TOP_LEVEL_ROLE_NAMES, *test_data.visited_order]
-            exp_calls = [(role, 1) for role in test_data.visited_order]
-            exp_target = 
self.sim.target_files[test_data.targetpath].target_file
-
-            updater = self._init_updater()
-            # Call explicitly refresh to simplify the expected_calls list
-            updater.refresh()
-            self.sim.fetch_tracker.metadata.clear()
-            target = updater.get_targetinfo(test_data.targetpath)
-            if target is not None:
-                # Confirm that the expected TargetFile is found
-                self.assertTrue(test_data.found)
-                self.assertDictEqual(target.to_dict(), exp_target.to_dict())
-            else:
-                self.assertFalse(test_data.found)
-            # Check that the delegated roles were visited in the expected
-            # order and the corresponding metadata files were persisted
-            self.assertListEqual(self.sim.fetch_tracker.metadata, exp_calls)
-            self._assert_files_exist(exp_files)
-        finally:
-            self.teardown_subtest()
+        self.setup_subtest()
+        exp_files = [*TOP_LEVEL_ROLE_NAMES, *test_data.visited_order]
+        exp_calls = [(role, 1) for role in test_data.visited_order]
+        exp_target = self.sim.target_files[test_data.targetpath].target_file
+
+        updater = self._init_updater()
+        # Call explicitly refresh to simplify the expected_calls list
+        updater.refresh()
+        self.sim.fetch_tracker.metadata.clear()
+        target = updater.get_targetinfo(test_data.targetpath)
+        if target is not None:
+            # Confirm that the expected TargetFile is found
+            self.assertTrue(test_data.found)
+            self.assertDictEqual(target.to_dict(), exp_target.to_dict())
+        else:
+            self.assertFalse(test_data.found)
+        # Check that the delegated roles were visited in the expected
+        # order and the corresponding metadata files were persisted
+        self.assertListEqual(self.sim.fetch_tracker.metadata, exp_calls)
+        self._assert_files_exist(exp_files)
 
 
 if __name__ == "__main__":
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/tests/test_updater_ng.py 
new/python-tuf-7.0.1/tests/test_updater_ng.py
--- old/python-tuf-7.0.0/tests/test_updater_ng.py       2026-05-18 
09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/tests/test_updater_ng.py       2026-09-01 
13:32:56.000000000 +0200
@@ -362,6 +362,29 @@
 
         self.assertEqual(ua[:23], "MyApp/1.2.3 python-tuf/")
 
+    @patch("os.symlink")
+    def test_update_root_symlink_oserror_fallback(
+        self, mock_symlink: MagicMock
+    ) -> None:
+        """Test fallback to os.link when os.symlink raises OSError."""
+        err = OSError("A required privilege is not held by the client")
+        err.winerror = 1314
+        mock_symlink.side_effect = err
+
+        self.updater._update_root_symlink()
+        mock_symlink.assert_called_once()
+
+        linkname = os.path.join(self.updater._dir, "root.json")
+        self.assertTrue(os.path.isfile(linkname))
+        self.assertFalse(os.path.islink(linkname))
+
+        version = self.updater._trusted_set.root.version
+        target = os.path.join(
+            self.updater._dir, "root_history", f"{version}.root.json"
+        )
+        with open(linkname, "rb") as f1, open(target, "rb") as f2:
+            self.assertEqual(f1.read(), f2.read())
+
 
 if __name__ == "__main__":
     utils.configure_test_logging(sys.argv)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/tests/test_utils.py 
new/python-tuf-7.0.1/tests/test_utils.py
--- old/python-tuf-7.0.0/tests/test_utils.py    2026-05-18 09:58:45.000000000 
+0200
+++ new/python-tuf-7.0.1/tests/test_utils.py    2026-09-01 13:32:56.000000000 
+0200
@@ -43,6 +43,36 @@
             sock.close()
 
 
+class TestRunSubTestsWithDataset(unittest.TestCase):
+    def test_teardown_subtest_runs_after_failure(self) -> None:
+        events = []
+
+        class SubTestCase(unittest.TestCase):
+            def teardown_subtest(self) -> None:
+                events.append(f"cleanup:{self.case_name}")
+
+            @utils.run_sub_tests_with_dataset(
+                {"failing": False, "passing": True}
+            )
+            def test_subtests(self, succeeds: bool) -> None:
+                events.append(f"run:{self.case_name}")
+                self.assertTrue(succeeds)
+
+        result = unittest.TestResult()
+        SubTestCase("test_subtests").run(result)
+
+        self.assertEqual(
+            [
+                "run:failing",
+                "cleanup:failing",
+                "run:passing",
+                "cleanup:passing",
+            ],
+            events,
+        )
+        self.assertEqual(len(result.failures), 1)
+
+
 class TestServerProcess(unittest.TestCase):
     """Test functionality provided in TestServerProcess from tests/utils.py."""
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/tests/utils.py 
new/python-tuf-7.0.1/tests/utils.py
--- old/python-tuf-7.0.0/tests/utils.py 2026-05-18 09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/tests/utils.py 2026-09-01 13:32:56.000000000 +0200
@@ -64,7 +64,11 @@
                 with test_cls.subTest(case=case):
                     # Save case name for future reference
                     test_cls.case_name = case.replace(" ", "_")
-                    function(test_cls, data)
+                    try:
+                        function(test_cls, data)
+                    finally:
+                        if hasattr(test_cls, "teardown_subtest"):
+                            test_cls.teardown_subtest()
 
         return wrapper
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/tuf/__init__.py 
new/python-tuf-7.0.1/tuf/__init__.py
--- old/python-tuf-7.0.0/tuf/__init__.py        2026-05-18 09:58:45.000000000 
+0200
+++ new/python-tuf-7.0.1/tuf/__init__.py        2026-09-01 13:32:56.000000000 
+0200
@@ -1,7 +1,7 @@
-# Copyright New York University and the TUF contributors
+# Copyright 2013-2026, New York University and the TUF contributors
 # SPDX-License-Identifier: MIT OR Apache-2.0
 
 """TUF."""
 
 # This value is used in the ngclient user agent.
-__version__ = "7.0.0"
+__version__ = "7.0.1"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/tuf/api/__init__.py 
new/python-tuf-7.0.1/tuf/api/__init__.py
--- old/python-tuf-7.0.0/tuf/api/__init__.py    2026-05-18 09:58:45.000000000 
+0200
+++ new/python-tuf-7.0.1/tuf/api/__init__.py    2026-09-01 13:32:56.000000000 
+0200
@@ -0,0 +1,2 @@
+# Copyright 2013-2020, New York University and the TUF contributors
+# SPDX-License-Identifier: MIT OR Apache-2.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/tuf/api/_payload.py 
new/python-tuf-7.0.1/tuf/api/_payload.py
--- old/python-tuf-7.0.0/tuf/api/_payload.py    2026-05-18 09:58:45.000000000 
+0200
+++ new/python-tuf-7.0.1/tuf/api/_payload.py    2026-09-01 13:32:56.000000000 
+0200
@@ -1,4 +1,4 @@
-# Copyright the TUF contributors
+# Copyright 2020-2026, the TUF contributors
 # SPDX-License-Identifier: MIT OR Apache-2.0
 
 
@@ -311,7 +311,7 @@
         )
 
     def __hash__(self) -> int:
-        return hash((self.keyids, self.threshold, self.unrecognized_fields))
+        return hash((tuple(self.keyids), self.threshold))
 
     @classmethod
     def from_dict(cls, role_dict: dict[str, Any]) -> Role:
@@ -530,7 +530,7 @@
 
     type = _ROOT
 
-    def __init__(
+    def __init__(  # noqa: PLR0917
         self,
         version: int | None = None,
         spec_version: str | None = None,
@@ -1089,7 +1089,7 @@
         ValueError: Invalid arguments.
     """
 
-    def __init__(
+    def __init__(  # noqa: PLR0917
         self,
         name: str,
         keyids: list[str],
@@ -1131,13 +1131,19 @@
         )
 
     def __hash__(self) -> int:
+        paths = tuple(self.paths) if self.paths is not None else None
+        prefixes = (
+            tuple(self.path_hash_prefixes)
+            if self.path_hash_prefixes is not None
+            else None
+        )
         return hash(
             (
                 super().__hash__(),
                 self.name,
                 self.terminating,
-                self.path,
-                self.path_hash_prefixes,
+                paths,
+                prefixes,
             )
         )
 
@@ -1710,7 +1716,7 @@
 
     type = _TARGETS
 
-    def __init__(
+    def __init__(  # noqa: PLR0917
         self,
         version: int | None = None,
         spec_version: str | None = None,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/tuf/api/dsse.py 
new/python-tuf-7.0.1/tuf/api/dsse.py
--- old/python-tuf-7.0.0/tuf/api/dsse.py        2026-05-18 09:58:45.000000000 
+0200
+++ new/python-tuf-7.0.1/tuf/api/dsse.py        2026-09-01 13:32:56.000000000 
+0200
@@ -1,3 +1,6 @@
+# Copyright 2023-2025, New York University and the TUF contributors
+# SPDX-License-Identifier: MIT OR Apache-2.0
+
 """Low-level TUF DSSE API. (experimental!)"""
 
 from __future__ import annotations
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/tuf/api/exceptions.py 
new/python-tuf-7.0.1/tuf/api/exceptions.py
--- old/python-tuf-7.0.0/tuf/api/exceptions.py  2026-05-18 09:58:45.000000000 
+0200
+++ new/python-tuf-7.0.1/tuf/api/exceptions.py  2026-09-01 13:32:56.000000000 
+0200
@@ -1,4 +1,4 @@
-# Copyright New York University and the TUF contributors
+# Copyright 2021-2025, New York University and the TUF contributors
 # SPDX-License-Identifier: MIT OR Apache-2.0
 
 """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/tuf/api/metadata.py 
new/python-tuf-7.0.1/tuf/api/metadata.py
--- old/python-tuf-7.0.0/tuf/api/metadata.py    2026-05-18 09:58:45.000000000 
+0200
+++ new/python-tuf-7.0.1/tuf/api/metadata.py    2026-09-01 13:32:56.000000000 
+0200
@@ -1,4 +1,4 @@
-# Copyright New York University and the TUF contributors
+# Copyright 2020-2025, New York University and the TUF contributors
 # SPDX-License-Identifier: MIT OR Apache-2.0
 
 """The low-level Metadata API.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/tuf/api/serialization/__init__.py 
new/python-tuf-7.0.1/tuf/api/serialization/__init__.py
--- old/python-tuf-7.0.0/tuf/api/serialization/__init__.py      2026-05-18 
09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/tuf/api/serialization/__init__.py      2026-09-01 
13:32:56.000000000 +0200
@@ -1,4 +1,4 @@
-# Copyright New York University and the TUF contributors
+# Copyright 2021-2024, New York University and the TUF contributors
 # SPDX-License-Identifier: MIT OR Apache-2.0
 
 """``tuf.api.serialization`` module provides abstract base classes and concrete
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/tuf/api/serialization/json.py 
new/python-tuf-7.0.1/tuf/api/serialization/json.py
--- old/python-tuf-7.0.0/tuf/api/serialization/json.py  2026-05-18 
09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/tuf/api/serialization/json.py  2026-09-01 
13:32:56.000000000 +0200
@@ -1,4 +1,4 @@
-# Copyright New York University and the TUF contributors
+# Copyright 2021-2025, New York University and the TUF contributors
 # SPDX-License-Identifier: MIT OR Apache-2.0
 
 """``tuf.api.serialization.json`` module provides concrete implementations to
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/tuf/ngclient/__init__.py 
new/python-tuf-7.0.1/tuf/ngclient/__init__.py
--- old/python-tuf-7.0.0/tuf/ngclient/__init__.py       2026-05-18 
09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/tuf/ngclient/__init__.py       2026-09-01 
13:32:56.000000000 +0200
@@ -1,4 +1,4 @@
-# Copyright New York University and the TUF contributors
+# Copyright 2021-2025, New York University and the TUF contributors
 # SPDX-License-Identifier: MIT OR Apache-2.0
 
 """TUF client public API."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/tuf/ngclient/_internal/__init__.py 
new/python-tuf-7.0.1/tuf/ngclient/_internal/__init__.py
--- old/python-tuf-7.0.0/tuf/ngclient/_internal/__init__.py     2026-05-18 
09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/tuf/ngclient/_internal/__init__.py     2026-09-01 
13:32:56.000000000 +0200
@@ -0,0 +1,2 @@
+# Copyright 2013-2021, New York University and the TUF contributors
+# SPDX-License-Identifier: MIT OR Apache-2.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/tuf/ngclient/_internal/proxy.py 
new/python-tuf-7.0.1/tuf/ngclient/_internal/proxy.py
--- old/python-tuf-7.0.0/tuf/ngclient/_internal/proxy.py        2026-05-18 
09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/tuf/ngclient/_internal/proxy.py        2026-09-01 
13:32:56.000000000 +0200
@@ -1,4 +1,4 @@
-# Copyright New York University and the TUF contributors
+# Copyright 2025, New York University and the TUF contributors
 # SPDX-License-Identifier: MIT OR Apache-2.0
 
 """Proxy environment variable handling with Urllib3"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-tuf-7.0.0/tuf/ngclient/_internal/trusted_metadata_set.py 
new/python-tuf-7.0.1/tuf/ngclient/_internal/trusted_metadata_set.py
--- old/python-tuf-7.0.0/tuf/ngclient/_internal/trusted_metadata_set.py 
2026-05-18 09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/tuf/ngclient/_internal/trusted_metadata_set.py 
2026-09-01 13:32:56.000000000 +0200
@@ -1,4 +1,4 @@
-# Copyright the TUF contributors
+# Copyright 2021-2026, the TUF contributors
 # SPDX-License-Identifier: MIT OR Apache-2.0
 
 """Trusted collection of client-side TUF Metadata.
@@ -115,6 +115,7 @@
                 error type and content will contain more details.
         """
         self._trusted_set: dict[str, Signed] = {}
+        self._trusted_delegations: set[tuple[str, str]] = set()
         self.reference_time = datetime.datetime.now(datetime.timezone.utc)
 
         if envelope_type is EnvelopeType.SIMPLE:
@@ -127,6 +128,17 @@
         logger.debug("Updating initial trusted root")
         self._load_trusted_root(root_data)
 
+    def contains(self, role: str, delegator: str) -> bool:
+        """Check if ``role`` is in ``TrustedMetadataSet`` and was verified
+        against ``delegator``.
+        """
+        return (delegator, role) in self._trusted_delegations
+
+    def _add(self, role: str, signed: Signed, delegator: str) -> None:
+        """Add role to trusted set, keep track of delegator(s)"""
+        self._trusted_set[role] = signed
+        self._trusted_delegations.add((delegator, role))
+
     def __getitem__(self, role: str) -> Signed:
         """Return current ``Signed`` for ``role``."""
         return self._trusted_set[role]
@@ -196,7 +208,7 @@
         # Verify that new root is signed by itself
         new_root.verify_delegate(Root.type, new_root_bytes, 
new_root_signatures)
 
-        self._trusted_set[Root.type] = new_root
+        self._add(Root.type, new_root, Root.type)
         logger.debug("Updated root v%d", new_root.version)
 
         return new_root
@@ -259,7 +271,7 @@
         # expiry not checked to allow old timestamp to be used for rollback
         # protection of new timestamp: expiry is checked in update_snapshot()
 
-        self._trusted_set[Timestamp.type] = new_timestamp
+        self._add(Timestamp.type, new_timestamp, Root.type)
         logger.debug("Updated timestamp v%d", new_timestamp.version)
 
         # timestamp is loaded: raise if it is not valid _final_ timestamp
@@ -346,7 +358,7 @@
         # expiry not checked to allow old snapshot to be used for rollback
         # protection of new snapshot: it is checked when targets is updated
 
-        self._trusted_set[Snapshot.type] = new_snapshot
+        self._add(Snapshot.type, new_snapshot, Root.type)
         logger.debug("Updated snapshot v%d", new_snapshot.version)
 
         # snapshot is loaded, but we raise if it's not valid _final_ snapshot
@@ -434,7 +446,7 @@
         if new_delegate.is_expired(self.reference_time):
             raise exceptions.ExpiredMetadataError(f"New {role_name} is 
expired")
 
-        self._trusted_set[role_name] = new_delegate
+        self._add(role_name, new_delegate, delegator_name)
         logger.debug("Updated %s v%d", role_name, version)
 
         return new_delegate
@@ -450,7 +462,7 @@
         )
         new_root.verify_delegate(Root.type, new_root_bytes, 
new_root_signatures)
 
-        self._trusted_set[Root.type] = new_root
+        self._add(Root.type, new_root, Root.type)
         logger.debug("Loaded trusted root v%d", new_root.version)
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/tuf/ngclient/updater.py 
new/python-tuf-7.0.1/tuf/ngclient/updater.py
--- old/python-tuf-7.0.0/tuf/ngclient/updater.py        2026-05-18 
09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/tuf/ngclient/updater.py        2026-09-01 
13:32:56.000000000 +0200
@@ -103,7 +103,7 @@
         RepositoryError: Local root.json is invalid
     """
 
-    def __init__(
+    def __init__(  # noqa: PLR0917
         self,
         metadata_dir: str,
         metadata_base_url: str,
@@ -369,7 +369,14 @@
         current = os.path.join("root_history", f"{version}.root.json")
         with contextlib.suppress(FileNotFoundError):
             os.remove(linkname)
-        os.symlink(current, linkname)
+        try:
+            os.symlink(current, linkname)
+        except OSError as e:
+            if getattr(e, "winerror", None) == 1314:
+                # Fallback for NTFS "required privilege is not held by client"
+                os.link(os.path.join(self._dir, current), linkname)
+            else:
+                raise
 
     def _load_root(self) -> None:
         """Load root metadata.
@@ -462,8 +469,8 @@
     def _load_targets(self, role: str, parent_role: str) -> Targets:
         """Load local (and if needed remote) metadata for ``role``."""
 
-        # Avoid loading 'role' more than once during "get_targetinfo"
-        if role in self._trusted_set:
+        # Avoid loading 'role' more than once for the same parent_role
+        if self._trusted_set.contains(role, parent_role):
             return cast("Targets", self._trusted_set[role])
 
         try:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-tuf-7.0.0/tuf/ngclient/urllib3_fetcher.py 
new/python-tuf-7.0.1/tuf/ngclient/urllib3_fetcher.py
--- old/python-tuf-7.0.0/tuf/ngclient/urllib3_fetcher.py        2026-05-18 
09:58:45.000000000 +0200
+++ new/python-tuf-7.0.1/tuf/ngclient/urllib3_fetcher.py        2026-09-01 
13:32:56.000000000 +0200
@@ -82,6 +82,9 @@
         except urllib3.exceptions.MaxRetryError as e:
             if isinstance(e.reason, urllib3.exceptions.TimeoutError):
                 raise exceptions.SlowRetrievalError from e
+            # Any other reason (e.g. TLS or connection error): let the original
+            # error propagate instead of falling through to an unbound 
response.
+            raise
 
         if response.status >= 400:
             response.close()
@@ -103,9 +106,16 @@
 
         try:
             yield from response.stream(self.chunk_size)
+        except urllib3.exceptions.TimeoutError as e:
+            # Raised directly when the gap timeout expires mid-stream:
+            # ReadTimeoutError is a TimeoutError but not a MaxRetryError.
+            raise exceptions.SlowRetrievalError from e
         except urllib3.exceptions.MaxRetryError as e:
             if isinstance(e.reason, urllib3.exceptions.TimeoutError):
                 raise exceptions.SlowRetrievalError from e
+            # Any other reason: propagate rather than ending the stream
+            # silently, which would look like a complete download.
+            raise
 
         finally:
             response.release_conn()

Reply via email to