Skip to content

Commit 69c32a4

Browse files
authored
Enable detached catalog signing for PyManager index files (#366)
1 parent 28d824d commit 69c32a4

File tree

4 files changed

+263
-120
lines changed

4 files changed

+263
-120
lines changed

windows-release/azure-pipelines.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ variables:
146146
IsRealSigned: false
147147
${{ if ne(parameters.SigningDescription, '(default)') }}:
148148
SigningDescription: ${{ parameters.SigningDescription }}
149+
${{ else }}:
150+
SigningDescription: ''
149151
PublishARM64: ${{ parameters.DoARM64 }}
150152
# QUEUE TIME VARIABLES
151153
# OverrideNugetVersion: ''
@@ -320,6 +322,7 @@ stages:
320322
BuildToPublish: ${{ parameters.BuildToPublish }}
321323
DoEmbed: ${{ parameters.DoEmbed }}
322324
DoFreethreaded: ${{ parameters.DoFreethreaded }}
325+
SigningCertificate: ${{ iif(eq(parameters.SigningCertificate, 'Unsigned'), '', parameters.SigningCertificate) }}
323326
- ${{ if eq(parameters.DoMSI, 'true') }}:
324327
- template: stage-publish-pythonorg.yml
325328
parameters:

windows-release/merge-and-upload.py

Lines changed: 91 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,18 @@
2020
UPLOAD_USER = os.getenv("UPLOAD_USER", "")
2121
NO_UPLOAD = os.getenv("NO_UPLOAD", "no")[:1].lower() in "yt1"
2222
LOCAL_INDEX = os.getenv("LOCAL_INDEX", "no")[:1].lower() in "yt1"
23+
SIGN_COMMAND = os.getenv("SIGN_COMMAND", "")
2324

2425

2526
def find_cmd(env, exe):
2627
cmd = os.getenv(env)
2728
if cmd:
28-
return Path(cmd)
29+
cmd = Path(cmd)
30+
if not cmd.is_file():
31+
raise RuntimeError(
32+
f"Could not find {cmd} to perform upload. Incorrect %{env}% setting."
33+
)
34+
return cmd
2935
for p in os.getenv("PATH", "").split(";"):
3036
if p:
3137
cmd = Path(p) / exe
@@ -40,6 +46,7 @@ def find_cmd(env, exe):
4046

4147
PLINK = find_cmd("PLINK", "plink.exe")
4248
PSCP = find_cmd("PSCP", "pscp.exe")
49+
MAKECAT = find_cmd("MAKECAT", "makecat.exe")
4350

4451

4552
def _std_args(cmd):
@@ -60,7 +67,9 @@ class RunError(Exception):
6067
pass
6168

6269

63-
def _run(*args):
70+
def _run(*args, single_cmd=False):
71+
if single_cmd:
72+
args = args[0]
6473
with subprocess.Popen(
6574
args,
6675
stdout=subprocess.PIPE,
@@ -193,6 +202,43 @@ def calculate_uploads():
193202
)
194203

195204

205+
def sign_json(cat_file, *files):
206+
if not MAKECAT:
207+
if not UPLOAD_HOST or NO_UPLOAD:
208+
print("makecat.exe not found, but not uploading, so skip signing.")
209+
return
210+
raise RuntimeError("No makecat.exe found")
211+
if not SIGN_COMMAND:
212+
if not UPLOAD_HOST or NO_UPLOAD:
213+
print("No signing command set, but not uploading, so skip signing.")
214+
return
215+
raise RuntimeError("No SIGN_COMMAND set")
216+
217+
cat = Path(cat_file).absolute()
218+
cdf = cat.with_suffix(".cdf")
219+
cdf.parent.mkdir(parents=True, exist_ok=True)
220+
221+
with open(cdf, "w", encoding="ansi") as f:
222+
print("[CatalogHeader]", file=f)
223+
print("Name=", cat.name, sep="", file=f)
224+
print("ResultDir=", cat.parent, sep="", file=f)
225+
print("PublicVersion=0x00000001", file=f)
226+
print("CatalogVersion=2", file=f)
227+
print("HashAlgorithms=SHA256", file=f)
228+
print("EncodingType=", file=f)
229+
print(file=f)
230+
print("[CatalogFiles]", file=f)
231+
for a in map(Path, files):
232+
print("<HASH>", a.name, "=", a.absolute(), sep="", file=f)
233+
234+
_run(MAKECAT, "-v", cdf)
235+
if not cat.is_file():
236+
raise FileNotFoundError(cat)
237+
# Pass as a single arg because the command variable has its own arguments
238+
_run(f'{SIGN_COMMAND} "{cat}"', single_cmd=True)
239+
cdf.unlink()
240+
241+
196242
def remove_and_insert(index, new_installs):
197243
new = {(i["id"].casefold(), i["sort-version"].casefold()) for i in new_installs}
198244
to_remove = [
@@ -274,6 +320,7 @@ def find_missing_from_index(url, installs):
274320
except FileNotFoundError:
275321
pass
276322

323+
277324
print(INDEX_PATH, "mtime =", INDEX_MTIME)
278325

279326

@@ -284,10 +331,20 @@ def find_missing_from_index(url, installs):
284331

285332
if INDEX_FILE:
286333
INDEX_FILE = Path(INDEX_FILE).absolute()
334+
INDEX_CAT_FILE = INDEX_FILE.with_name(f"{INDEX_FILE.name}.cat")
287335
INDEX_FILE.parent.mkdir(parents=True, exist_ok=True)
288336
with open(INDEX_FILE, "w", encoding="utf-8") as f:
289337
json.dump(index, f)
290338

339+
sign_json(INDEX_CAT_FILE, INDEX_FILE)
340+
INDEX_CAT_URL = f"{INDEX_URL}.cat"
341+
INDEX_CAT_PATH = f"{INDEX_PATH}.cat"
342+
else:
343+
INDEX_CAT_FILE = None
344+
INDEX_CAT_URL = None
345+
INDEX_CAT_PATH = None
346+
347+
291348
if MANIFEST_FILE:
292349
# Use the sort-version so that the manifest name includes prerelease marks
293350
MANIFEST_FILE = Path(MANIFEST_FILE).absolute()
@@ -323,33 +380,36 @@ def find_missing_from_index(url, installs):
323380
print("Expecting mtime", INDEX_MTIME, "but saw", mtime)
324381
sys.exit(1)
325382

383+
TO_PURGE = [i["url"] for i, *_ in UPLOADS]
384+
385+
if MANIFEST_FILE:
386+
print("Uploading", MANIFEST_FILE, "to", MANIFEST_URL)
387+
upload_ssh(MANIFEST_FILE, MANIFEST_PATH)
388+
TO_PURGE.append(MANIFEST_URL)
326389

327-
if not NO_UPLOAD:
328-
if MANIFEST_FILE:
329-
print("Uploading", MANIFEST_FILE, "to", MANIFEST_URL)
330-
upload_ssh(MANIFEST_FILE, MANIFEST_PATH)
331-
332-
if INDEX_FILE:
333-
print("Uploading", INDEX_FILE, "to", INDEX_URL)
334-
upload_ssh(INDEX_FILE, INDEX_PATH)
335-
336-
print("Purging", len(UPLOADS), "uploaded files")
337-
parents = set()
338-
for i, *_ in UPLOADS:
339-
purge(i["url"])
340-
parents.add(i["url"].rpartition("/")[0] + "/")
341-
for i in parents:
342-
purge(i)
343-
if MANIFEST_URL:
344-
purge(MANIFEST_URL)
345-
purge(MANIFEST_URL.rpartition("/")[0] + "/")
346-
if INDEX_URL:
347-
purge(INDEX_URL)
348-
purge(INDEX_URL.rpartition("/")[0] + "/")
349-
missing = find_missing_from_index(INDEX_URL, [i for i, *_ in UPLOADS])
350-
if missing:
351-
print("##[error]Lost a race with another publish step!")
352-
print("Index at", INDEX_URL, "does not contain installs:")
353-
for m in missing:
354-
print(m["id"], m["sort-version"])
355-
sys.exit(1)
390+
if INDEX_FILE:
391+
print("Uploading", INDEX_FILE, "to", INDEX_URL)
392+
upload_ssh(INDEX_FILE, INDEX_PATH)
393+
TO_PURGE.append(INDEX_URL)
394+
395+
if INDEX_CAT_FILE:
396+
print("Uploading", INDEX_CAT_FILE, "to", INDEX_CAT_URL)
397+
upload_ssh(INDEX_CAT_FILE, INDEX_CAT_PATH)
398+
TO_PURGE.append(INDEX_CAT_URL)
399+
400+
# Calculate directory parents for all files
401+
TO_PURGE.extend({i.rpartition("/")[0] + "/" for i in TO_PURGE})
402+
403+
print("Purging", len(TO_PURGE), "uploaded files, indexes and directories")
404+
405+
for i in TO_PURGE:
406+
purge(i)
407+
408+
if INDEX_URL:
409+
missing = find_missing_from_index(INDEX_URL, [i for i, *_ in UPLOADS])
410+
if missing:
411+
print("##[error]Lost a race with another publish step!")
412+
print("Index at", INDEX_URL, "does not contain installs:")
413+
for m in missing:
414+
print(m["id"], m["sort-version"])
415+
sys.exit(1)

0 commit comments

Comments
 (0)