Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions SPECS/python-virtualenv/python-virtualenv.spec
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Summary: Virtual Python Environment builder
Name: python-virtualenv
Version: 20.36.1
Release: 8%{?dist}
Release: 9%{?dist}
License: MIT
Vendor: Microsoft Corporation
Distribution: Azure Linux
Expand All @@ -24,6 +24,7 @@ Patch1011: CVE-2026-13346v0.patch
Patch1012: CVE-2026-13346v1.patch
Patch1013: CVE-2026-102925.patch
Patch1014: CVE-2026-102930.patch
Patch1015: remove-compromised-wheel-ca-roots.patch
BuildArch: noarch

%description
Expand Down Expand Up @@ -58,6 +59,7 @@ virtualenv is a tool to create isolated Python environment.
%patch -P 0 -p1
%patch -P 1013 -p1
%patch -P 1014 -p1
%patch -P 1015 -p1

# Manual patching for CVE-2025-50181 and CVE-2026-1703v0
# For CVE-2025-50181, poolmanager.py file is located in 2 different places and each is of different version so the same patch cannot be applied to all of them.
Expand Down Expand Up @@ -158,6 +160,9 @@ zip -r ../src/virtualenv/seed/wheels/embed/unpacked_wheel-0.45.1-py3-none-any.wh
popd
rm -rf unpacked_wheel-0.45.1-py3-none-any

%{__python3} Tools/remove-untrusted-wheel-roots.py \
--expected-removals 3 src/virtualenv/seed/wheels/embed/pip-*.whl

%generate_buildrequires

%build
Expand All @@ -183,9 +188,15 @@ tox -e py
%{_bindir}/virtualenv

%changelog
* Mon Oct 12 2026 Kanishk Bansal <kanbansal@microsoft.com> - 20.36.1-8
- Pin packaging==23.2 when pip-installing test tooling in %%check to fix
ptest failure (uninstall-no-record-file) caused by virtualenv 21.14.6.
* Fri Oct 30 2026 Kanishk Bansal <kanbansal@microsoft.com> - 20.36.1-9
- Preserve the dev fix for RPM-owned packaging during ptest bootstrap.
- Use the published private, pinned test environment in place of the
system-pip packaging==23.2/tox bootstrap.

* Fri Oct 23 2026 Jon Slobodzian <joslobo@microsoft.com> - 20.36.1-8
- Remove affected eMudhra and AAA roots from both embedded pip wheels.
- Regenerate wheel RECORD entries after the existing security backports.
- Run pytest directly with pinned test dependencies in a private environment.

* Mon Oct 05 2026 Kshitiz Godara <kgodara@microsoft.com> - 20.36.1-7
- Patch for CVE-2026-102925 and CVE-2026-102930
Expand Down
119 changes: 119 additions & 0 deletions SPECS/python-virtualenv/remove-compromised-wheel-ca-roots.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
Subject: [PATCH] Distrust incident-related eMudhra and AAA trust anchors

Remove only the affected trust anchors, including the AAA cross-signing
path. Preserve the unaffected emSign G1, C1 and C3 anchors.
Reference: https://emudhra.we-have-a-problem.com/
Upstream-status: Azure Linux trust-store backport

--- /dev/null
+++ b/Tools/remove-untrusted-wheel-roots.py
@@ -0,0 +1,109 @@
+"""Remove incident-related trust anchors from bundled wheels and renew RECORD."""
+
+import argparse
+import base64
+import csv
+import hashlib
+import io
+from pathlib import Path
+import re
+import ssl
+import tempfile
+import zipfile
+
+
+UNTRUSTED_SHA256 = {
+ "d7a7a0fb5d7e2731d771e9484ebcdef71d5f0c3e0a2948782bc83ee0ea699ef4",
+ "86a1ecba089c4a8d3bbe2734c612ba341d813e043cf9e8a862cd5c57a36bbe6b",
+}
+CERTIFICATE = re.compile(
+ rb"-----BEGIN CERTIFICATE-----\s+[A-Za-z0-9+/=\s]+?-----END CERTIFICATE-----"
+)
+
+
+def prepare_wheel(path):
+ with zipfile.ZipFile(path) as archive:
+ infos = archive.infolist()
+ if len({info.filename for info in infos}) != len(infos):
+ raise ValueError(f"{path}: duplicate wheel members")
+ records = [
+ info.filename
+ for info in infos
+ if info.filename.endswith(".dist-info/RECORD")
+ ]
+ if len(records) != 1:
+ raise ValueError(f"{path}: expected exactly one wheel RECORD")
+ record = records[0]
+ if any(
+ name in archive.namelist() for name in (record + ".jws", record + ".p7s")
+ ):
+ raise ValueError(f"{path}: cannot rewrite a signed wheel")
+ payload = {info.filename: archive.read(info) for info in infos}
+
+ removed = 0
+
+ def retain_certificate(match):
+ nonlocal removed
+ der = ssl.PEM_cert_to_DER_cert(match.group().decode("ascii"))
+ if hashlib.sha256(der).hexdigest() in UNTRUSTED_SHA256:
+ removed += 1
+ return b""
+ return match.group()
+
+ for name, data in payload.items():
+ if name.endswith((".pem", ".crt")):
+ payload[name] = CERTIFICATE.sub(retain_certificate, data)
+
+ if not removed:
+ return None, 0
+
+ output = io.StringIO(newline="")
+ writer = csv.writer(output, lineterminator="\n")
+ for info in infos:
+ name = info.filename
+ if info.is_dir():
+ continue
+ if name == record:
+ writer.writerow((name, "", ""))
+ else:
+ data = payload[name]
+ digest = base64.urlsafe_b64encode(hashlib.sha256(data).digest())
+ writer.writerow((name, "sha256=" + digest.rstrip(b"=").decode(), len(data)))
+ payload[record] = output.getvalue().encode("utf-8")
+ return (path, infos, payload), removed
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--expected-removals", type=int, required=True)
+ parser.add_argument("wheels", nargs="+", type=Path)
+ args = parser.parse_args()
+ plans = []
+ total = 0
+ for path in args.wheels:
+ plan, removed = prepare_wheel(path)
+ if plan is not None:
+ plans.append(plan)
+ total += removed
+ print(f"{path}: removing {removed} affected trust anchors")
+ if total != args.expected_removals:
+ raise ValueError(
+ f"Expected {args.expected_removals} trust anchors, found {total}; "
+ "review the bundled wheels before changing this expectation"
+ )
+ for path, infos, payload in plans:
+ with tempfile.NamedTemporaryFile(
+ dir=path.parent, prefix=path.name + ".", suffix=".tmp", delete=False
+ ) as temporary:
+ temporary_path = Path(temporary.name)
+ try:
+ with zipfile.ZipFile(temporary_path, "w") as archive:
+ for info in infos:
+ archive.writestr(info, payload[info.filename])
+ temporary_path.replace(path)
+ finally:
+ temporary_path.unlink(missing_ok=True)
+
+
+if __name__ == "__main__":
+ main()
Loading