Skip to content

Commit 89f8401

Browse files
committed
Add catalog signing of PyManager index files
1 parent 80a86b4 commit 89f8401

File tree

3 files changed

+175
-52
lines changed

3 files changed

+175
-52
lines changed

windows-release/merge-and-upload.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
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):
@@ -40,6 +41,7 @@ def find_cmd(env, exe):
4041

4142
PLINK = find_cmd("PLINK", "plink.exe")
4243
PSCP = find_cmd("PSCP", "pscp.exe")
44+
MAKECAT = find_cmd("MAKECAT", "makecat.exe")
4345

4446

4547
def _std_args(cmd):
@@ -193,6 +195,42 @@ def calculate_uploads():
193195
)
194196

195197

198+
def sign_json(cat_file, *files):
199+
if not MAKECAT:
200+
if not UPLOAD_HOST or NO_UPLOAD:
201+
print("makecat.exe not found, but not uploading, so skip signing.")
202+
return
203+
raise RuntimeError("No makecat.exe found")
204+
if not SIGN_COMMAND:
205+
if not UPLOAD_HOST or NO_UPLOAD:
206+
print("No signing command set, but not uploading, so skip signing.")
207+
return
208+
raise RuntimeError("No SIGN_COMMAND set")
209+
210+
cat = Path(cat_file).absolute()
211+
cdf = cat.with_suffix(".cdf")
212+
cdf.parent.mkdir(parents=True, exist_ok=True)
213+
214+
with open(cdf, "w", encoding="ansi") as f:
215+
print("[CatalogHeader]", file=f)
216+
print("Name=", cat.name, sep="", file=f)
217+
print("ResultDir=", cat.parent, sep="", file=f)
218+
print("PublicVersion=0x00000001", file=f)
219+
print("CatalogVersion=2", file=f)
220+
print("HashAlgorithms=SHA256", file=f)
221+
print("EncodingType=", file=f)
222+
print(file=f)
223+
print("[CatalogFiles]", file=f)
224+
for a in map(Path, files):
225+
print("<HASH>", a.name, "=", a.absolute(), sep="", file=f)
226+
227+
_run(MAKECAT, "-v", cdf)
228+
if not cat.is_file():
229+
raise FileNotFoundError(cat)
230+
_run(SIGN_COMMAND, cat)
231+
cdf.unlink()
232+
233+
196234
def remove_and_insert(index, new_installs):
197235
new = {(i["id"].casefold(), i["sort-version"].casefold()) for i in new_installs}
198236
to_remove = [
@@ -274,20 +312,28 @@ def find_missing_from_index(url, installs):
274312
except FileNotFoundError:
275313
pass
276314

315+
277316
print(INDEX_PATH, "mtime =", INDEX_MTIME)
278317

279318

319+
280320
new_installs = [trim_install(i) for i, *_ in UPLOADS]
281321
validate_new_installs(new_installs)
282322
new_installs = sorted(new_installs, key=install_sortkey)
283323
remove_and_insert(index["versions"], new_installs)
284324

285325
if INDEX_FILE:
286326
INDEX_FILE = Path(INDEX_FILE).absolute()
327+
INDEX_CAT_FILE = INDEX_FILE.with_suffix(".cat")
287328
INDEX_FILE.parent.mkdir(parents=True, exist_ok=True)
288329
with open(INDEX_FILE, "w", encoding="utf-8") as f:
289330
json.dump(index, f)
290331

332+
sign_json(INDEX_CAT_FILE, INDEX_FILE)
333+
else:
334+
INDEX_CAT_FILE = None
335+
336+
291337
if MANIFEST_FILE:
292338
# Use the sort-version so that the manifest name includes prerelease marks
293339
MANIFEST_FILE = Path(MANIFEST_FILE).absolute()
@@ -333,6 +379,10 @@ def find_missing_from_index(url, installs):
333379
print("Uploading", INDEX_FILE, "to", INDEX_URL)
334380
upload_ssh(INDEX_FILE, INDEX_PATH)
335381

382+
if INDEX_CAT_FILE:
383+
print("Uploading", INDEX_CAT_FILE, "to", f"{INDEX_URL}.cat")
384+
upload_ssh(INDEX_CAT_FILE, f"{INDEX_PATH}.cat")
385+
336386
print("Purging", len(UPLOADS), "uploaded files")
337387
parents = set()
338388
for i, *_ in UPLOADS:

windows-release/sign-files.yml

Lines changed: 107 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,83 @@ parameters:
66
ExtractDir: ''
77
SigningCertificate: ''
88
ExportCommand: ''
9+
ExportLegacyCommand: ''
910
ContinueOnError: false
11+
InstallTool: true
12+
InstallLegacyTool: false
1013
AzureServiceConnectionName: 'Python Signing'
1114

1215
steps:
13-
- ${{ if parameters.SigningCertificate }}:
14-
- powershell: |
15-
# Install sign tool
16-
dotnet tool install --global --prerelease sign
17-
$signtool = (gcm sign -EA SilentlyContinue).Source
18-
if (-not $signtool) {
19-
$signtool = (gi "${env:USERPROFILE}\.dotnet\tools\sign.exe").FullName
20-
}
21-
$signargs = 'code trusted-signing -v Information ' + `
22-
'-fd sha256 -t http://timestamp.acs.microsoft.com -td sha256 ' + `
23-
'-tse "$(TrustedSigningUri)" -tsa "$(TrustedSigningAccount)" -tscp "$(TrustedSigningCertificateName)" ' + `
24-
'-d "$(SigningDescription)" '
16+
- ${{ if and(parameters.SigningCertificate, ne(parameters.SigningCertificate, 'Unsigned')) }}:
17+
- ${{ if eq(parameters.InstallTool, 'true') }}:
18+
- powershell: |
19+
# Install sign tool
20+
dotnet tool install --global --prerelease sign
21+
$signtool = (gcm sign -EA SilentlyContinue).Source
22+
if (-not $signtool) {
23+
$signtool = (gi "${env:USERPROFILE}\.dotnet\tools\sign.exe").FullName
24+
}
25+
$signargs = 'code artifact-signing -v Information ' + `
26+
'-fd sha256 -t http://timestamp.acs.microsoft.com -td sha256 ' + `
27+
"-tse ""${env:TSE}"" -tsa ""${env:TSA}"" -tscp ""${env:TSCP}"" -d ""${env:DESCRIPTION}"""
28+
29+
Write-Host "##vso[task.setvariable variable=__TrustedSigningCmd]$signtool"
30+
Write-Host "##vso[task.setvariable variable=__TrustedSigningArgs]$signargs"
31+
if ($env:EXPORT_COMMAND) {
32+
$signcmd = """$signtool"" $signargs"
33+
Write-Host "##vso[task.setvariable variable=${env:EXPORT_COMMAND}]$signcmd"
34+
}
35+
workingDirectory: $(Build.BinariesDirectory)
36+
displayName: 'Install Azure Artifact Signing tools'
37+
env:
38+
TSE: $(TrustedSigningUri)
39+
TSA: $(TrustedSigningAccount)
40+
TSCP: $(TrustedSigningCertificateName)
41+
DESCRIPTION: $(SigningDescription)
42+
EXPORT_COMMAND: ${{ parameters.ExportCommand }}
43+
44+
- ${{ if eq(parameters.InstallLegacyTool, 'true') }}:
45+
- powershell: |
46+
git clone https://github.com/python/cpython-bin-deps --revision fb06137dccc43ed5b030cdd9e3560990b37f39da --depth 1 --progress -v "signtool"
47+
48+
$signtool = gi signtool\x64\signtool.exe
49+
$dlib = gi signtool\azure_trusted_signing\x64\Azure.CodeSigning.Dlib.dll
50+
Write-Host "##vso[task.setvariable variable=MAKECAT]$(gi signtool\x64\makecat.exe)"
51+
52+
ConvertTo-Json @{
53+
Endpoint=$env:TSE;
54+
CodeSigningAccountName=$env:TSA;
55+
CertificateProfileName=$env:TSCP;
56+
# Only allow Azure CLI credentials and environment credentials
57+
ExcludeCredentials=@(
58+
"ManagedIdentityCredential",
59+
"WorkloadIdentityCredential",
60+
"SharedTokenCacheCredential",
61+
"VisualStudioCredential",
62+
"VisualStudioCodeCredential",
63+
"AzurePowerShellCredential",
64+
"AzureDeveloperCliCredential",
65+
"InteractiveBrowserCredential"
66+
);
67+
} | Out-File signtool\metadata.json -Encoding ascii
68+
Write-Host "##vso[task.setvariable variable=SIGNTOOL_METADATA]$(gi signtool\metadata.json)"
2569
26-
Write-Host "##vso[task.setvariable variable=__TrustedSigningCmd]$signtool"
27-
Write-Host "##vso[task.setvariable variable=__TrustedSigningArgs]$signargs"
28-
if ($env:EXPORT_COMMAND) {
29-
$signcmd = """$signtool"" $signargs"
30-
Write-Host "##vso[task.setvariable variable=${env:EXPORT_COMMAND}]$signcmd"
31-
}
32-
workingDirectory: $(Build.BinariesDirectory)
33-
displayName: 'Install Trusted Signing tools'
34-
env:
35-
EXPORT_COMMAND: ${{ parameters.ExportCommand }}
70+
$signargs = 'sign /v /fd sha256 /tr http://timestamp.acs.microsoft.com /td SHA256' + `
71+
"/dlib ""$dlib"" /dmdf ""$(gi signtool\metadata.json)"""
72+
Write-Host "##vso[task.setvariable variable=__TrustedSigningCmd]$signtool"
73+
Write-Host "##vso[task.setvariable variable=__TrustedSigningArgs]$signargs"
74+
75+
if ($env:EXPORT_COMMAND) {
76+
$signcmd = """$signtool"" $signargs"
77+
Write-Host "##vso[task.setvariable variable=${env:EXPORT_COMMAND}]$signcmd"
78+
}
79+
workingDirectory: $(Pipeline.Workspace)
80+
displayName: 'Download signtool binaries'
81+
env:
82+
TSE: $(TrustedSigningUri)
83+
TSA: $(TrustedSigningAccount)
84+
TSCP: $(TrustedSigningCertificateName)
85+
EXPORT_COMMAND: ${{ parameters.ExportLegacyCommand }}
3686
3787
- ${{ if parameters.AzureServiceConnectionName }}:
3888
# We sign in once with the AzureCLI task, as it uses OIDC to obtain a
@@ -45,25 +95,25 @@ steps:
4595
scriptType: 'ps'
4696
scriptLocation: 'inlineScript'
4797
inlineScript: |
48-
"##vso[task.setvariable variable=AZURE_CLIENT_ID;issecret=true]${env:servicePrincipalId}"
49-
"##vso[task.setvariable variable=AZURE_ID_TOKEN;issecret=true]${env:idToken}"
50-
"##vso[task.setvariable variable=AZURE_TENANT_ID;issecret=true]${env:tenantId}"
98+
"##vso[task.setvariable variable=__AZURE_CLIENT_ID;issecret=true]${env:servicePrincipalId}"
99+
"##vso[task.setvariable variable=__AZURE_ID_TOKEN;issecret=true]${env:idToken}"
100+
"##vso[task.setvariable variable=__AZURE_TENANT_ID;issecret=true]${env:tenantId}"
51101
addSpnToEnvironment: true
52102

53103
- powershell: >
54104
az login --service-principal
55-
-u $(AZURE_CLIENT_ID)
56-
--tenant $(AZURE_TENANT_ID)
105+
-u $(__AZURE_CLIENT_ID)
106+
--tenant $(__AZURE_TENANT_ID)
57107
--allow-no-subscriptions
58-
--federated-token $(AZURE_ID_TOKEN)
108+
--federated-token $(__AZURE_ID_TOKEN)
59109
displayName: 'Authenticate signing tools (2/2)'
60110
61111
- ${{ if parameters.Include }}:
62112
- powershell: |
63-
if ("${{ parameters.Exclude }}") {
64-
$files = (dir ${{ parameters.Include }} -Exclude ${{ parameters.Exclude }} -File)
113+
if ($env:EXCLUDE) {
114+
$files = (dir $env:INCLUDE -Exclude $env:EXCLUDE -File)
65115
} else {
66-
$files = (dir ${{ parameters.Include }} -File)
116+
$files = (dir $env:INCLUDE -File)
67117
}
68118
if ($env:FILTER) {
69119
($env:FILTER -split ';') -join "`n" | Out-File __filelist.txt -Encoding utf8
@@ -82,31 +132,37 @@ steps:
82132
continueOnError: true
83133
workingDirectory: ${{ parameters.WorkingDir }}
84134
env:
135+
INCLUDE: ${{ parameters.Include }}
136+
EXCLUDE: ${{ parameters.Exclude }}
85137
TRUSTED_SIGNING_CMD: $(__TrustedSigningCmd)
86138
TRUSTED_SIGNING_ARGS: $(__TrustedSigningArgs)
87139
${{ if parameters.Filter }}:
88140
FILTER: ${{ parameters.Filter }}
89141
90142
91-
- ${{ if parameters.ExtractDir }}:
92-
- powershell: |
93-
if ("${{ parameters.Exclude }}") {
94-
$files = (dir ${{ parameters.Include }} -Exclude ${{ parameters.Exclude }} -File)
95-
} else {
96-
$files = (dir ${{ parameters.Include }} -File)
97-
}
98-
$c = $files | %{ (Get-AuthenticodeSignature $_).SignerCertificate } | ?{ $_ -ne $null } | select -First 1
99-
if (-not $c) {
100-
Write-Host "Failed to find certificate for ${{ parameters.SigningCertificate }}"
101-
exit
102-
}
143+
- ${{ if parameters.ExtractDir }}:
144+
- powershell: |
145+
if ($env:EXCLUDE) {
146+
$files = (dir $env:INCLUDE -Exclude $env:EXCLUDE -File)
147+
} else {
148+
$files = (dir $env:INCLUDE -File)
149+
}
150+
$c = $files | %{ (Get-AuthenticodeSignature $_).SignerCertificate } | ?{ $_ -ne $null } | select -First 1
151+
if (-not $c) {
152+
Write-Host "Failed to find certificate for ${{ parameters.SigningCertificate }}"
153+
exit
154+
}
103155
104-
$d = mkdir "${{ parameters.ExtractDir }}" -Force
105-
$cf = "$d\cert.cer"
106-
[IO.File]::WriteAllBytes($cf, $c.RawData)
107-
$csha = (Get-FileHash $cf -Algorithm SHA256).Hash.ToLower()
156+
$d = mkdir $env:EXTRACT_DIR -Force
157+
$cf = "$d\cert.cer"
158+
[IO.File]::WriteAllBytes($cf, $c.RawData)
159+
$csha = (Get-FileHash $cf -Algorithm SHA256).Hash.ToLower()
108160
109-
$info = @{ Subject=$c.Subject; SHA256=$csha; }
110-
$info | ConvertTo-JSON -Compress | Out-File -Encoding utf8 "$d\certinfo.json"
111-
displayName: "Extract certificate info"
112-
workingDirectory: ${{ parameters.WorkingDir }}
161+
$info = @{ Subject=$c.Subject; SHA256=$csha; }
162+
$info | ConvertTo-JSON -Compress | Out-File -Encoding utf8 "$d\certinfo.json"
163+
displayName: "Extract certificate info"
164+
workingDirectory: ${{ parameters.WorkingDir }}
165+
env:
166+
INCLUDE: ${{ parameters.Include }}
167+
EXCLUDE: ${{ parameters.Exclude }}
168+
EXTRACT_DIR: ${{ parameters.ExtractDir }}

windows-release/stage-publish-pymanager.yml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ parameters:
33
DoFreethreaded: false
44
DoEmbed: false
55
HashAlgorithms: ['SHA256']
6+
SigningCertificate: ''
67

78
Artifacts:
89
- name: win32
@@ -31,6 +32,10 @@ jobs:
3132

3233
variables:
3334
- group: PythonOrgPublish
35+
- ${{ if eq(parameters.SigningCertificate, 'PythonSoftwareFoundation') }}:
36+
- group: CPythonSign
37+
- ${{ if eq(parameters.SigningCertificate, 'TestSign') }}:
38+
- group: CPythonTestSign
3439

3540
workspace:
3641
clean: all
@@ -64,11 +69,20 @@ jobs:
6469
displayName: 'Download PuTTY key'
6570

6671
- powershell: |
67-
git clone https://github.com/python/cpython-bin-deps --branch putty --single-branch --depth 1 --progress -v "putty"
72+
git clone https://github.com/python/cpython-bin-deps --revision 9f9e6fc31a55406ee5ff0198ea47bbb445eeb942 --depth 1 --progress -v "putty"
6873
"##vso[task.prependpath]$(gi putty)"
6974
workingDirectory: $(Pipeline.Workspace)
7075
displayName: 'Download PuTTY binaries'
7176
77+
# Use the template just to configure the signing tool.
78+
- template: sign-files.yml
79+
parameters:
80+
Include: ""
81+
InstallTool: false
82+
InstallLegacyTool: true
83+
ExportLegacyCommand: SIGN_COMMAND
84+
SigningCertificate: ${{ parameters.SigningCertificate }}
85+
7286
- powershell: |
7387
if ($env:FILENAME) {
7488
"##vso[task.setvariable variable=_PyManagerIndexFilename]${env:FILENAME}"
@@ -113,6 +127,9 @@ jobs:
113127
UPLOAD_HOST_KEY: $(PyDotOrgHostKey)
114128
UPLOAD_USER: $(PyDotOrgUsername)
115129
UPLOAD_KEYFILE: $(sshkey.secureFilePath)
130+
${{ if variables['SIGN_COMMAND'] }}:
131+
MAKECAT: $(MAKECAT)
132+
SIGN_COMMAND: $(SIGN_COMMAND)
116133
117134
- ${{ each alg in parameters.HashAlgorithms }}:
118135
- powershell: |

0 commit comments

Comments
 (0)