返回 DeepSeek-Reasonix
windows_signing_packaging_test.go
根目录 / desktop / windows_signing_packaging_test.go
1 package main
2
3 import (
4 "encoding/xml"
5 "errors"
6 "os"
7 "os/exec"
8 "path/filepath"
9 "strings"
10 "testing"
11 )
12
13 type signPathArtifactConfiguration struct {
14 Zip signPathZip `xml:"zip-file"`
15 }
16
17 type signPathZip struct {
18 Files []signPathPEFile `xml:"pe-file"`
19 }
20
21 type signPathPEFile struct {
22 Path string `xml:"path,attr"`
23 Sign *struct{} `xml:"authenticode-sign"`
24 Verify *struct{} `xml:"authenticode-verify"`
25 }
26
27 func readTestFile(t *testing.T, path string) string {
28 t.Helper()
29 data, err := os.ReadFile(path)
30 if err != nil {
31 t.Fatal(err)
32 }
33 return string(data)
34 }
35
36 func parseSignPathConfiguration(t *testing.T, name string) signPathArtifactConfiguration {
37 t.Helper()
38 data, err := os.ReadFile(filepath.Join("..", ".signpath", "artifact-configurations", name))
39 if err != nil {
40 t.Fatal(err)
41 }
42 var config signPathArtifactConfiguration
43 if err := xml.Unmarshal(data, &config); err != nil {
44 t.Fatalf("parse %s: %v", name, err)
45 }
46 return config
47 }
48
49 func TestWindowsReleaseSignsPayloadBeforeRepackaging(t *testing.T) {
50 workflow := readTestFile(t, "../.github/workflows/release-desktop.yml")
51 orderedSteps := []string{
52 "name: Upload unsigned Windows payload for SignPath",
53 "name: Submit Windows payload for Authenticode signing",
54 "name: Approve and download signed Windows payload",
55 "name: Bind signed Windows payload to release manifest",
56 "name: Rebuild Windows packages from signed payload",
57 "name: Upload unsigned installer for SignPath",
58 "name: Submit installer for Authenticode signing",
59 "name: Approve and download signed Windows installer",
60 "name: Replace installer with signed build",
61 "name: Checkout protected release verifier",
62 "name: Verify Windows Authenticode release contract",
63 "name: Sign artifacts (minisign)",
64 }
65 last := -1
66 for _, step := range orderedSteps {
67 index := strings.Index(workflow, step)
68 if index < 0 {
69 t.Fatalf("desktop release workflow is missing %q", step)
70 }
71 if index <= last {
72 t.Fatalf("desktop release workflow step %q is out of order", step)
73 }
74 last = index
75 }
76 for _, want := range []string{
77 `artifact-configuration-slug: windows-payload`,
78 `artifact-configuration-slug: windows-installer-v2`,
79 `path: desktop/build/windows/signing-payload/*.exe`,
80 `path: desktop/build/windows/installer-signing-bundle/*.exe`,
81 `github.repository == 'esengine/DeepSeek-Reasonix'`,
82 `SIGNPATH_API_TOKEN is required for public Windows Preview and Stable releases`,
83 `SIGNPATH_RELEASE_SIGNING_ATTESTATION does not match the current protected signing contract`,
84 `signing-policy-slug: release-signing`,
85 `needs.build.result == 'success' && !inputs.production_signing_smoke && !inputs.signing_preflight`,
86 `go run ./cmd/signpath-contract fingerprint`,
87 `wait-for-completion: false`,
88 `steps.submit-windows-payload.outputs.signing-request-id`,
89 `steps.submit-windows-installer.outputs.signing-request-id`,
90 `scripts/complete-signpath-request.ps1`,
91 `-WaitForExternalApproval:$waitForExternalApproval`,
92 `go run ./cmd/sign windows-payload ../signed-payload "${{ needs.resolve.outputs.version }}"`,
93 `go run ./cmd/sign sign ../signed-payload/reasonix-payload.json`,
94 `go run ./cmd/sign verify ../signed-payload/reasonix-payload.json`,
95 `REASONIX_REQUIRE_PAYLOAD_MANIFEST: "1"`,
96 `ref: ${{ github.sha }}`,
97 `path: release-control`,
98 `./release-control/scripts/verify-windows-authenticode.ps1`,
99 } {
100 if !strings.Contains(workflow, want) {
101 t.Errorf("desktop release workflow is missing signing contract %q", want)
102 }
103 }
104 for _, forbidden := range []string{
105 `signing-policy-slug: test-signing`,
106 `artifact-configuration-slug: windows-installer-test-v2`,
107 `steps.ver.outputs.channel == 'canary'`,
108 } {
109 if strings.Contains(workflow, forbidden) {
110 t.Errorf("public desktop release workflow contains legacy Canary signing contract %q", forbidden)
111 }
112 }
113
114 packager := readTestFile(t, "../scripts/package-windows-desktop.sh")
115 copyMain := strings.Index(packager, `cp "$PAYLOAD/$BINNAME.exe" "$BIN_DIR/$BINNAME.exe"`)
116 makeNSIS := strings.Index(packager, "makensis \\\n")
117 portable := strings.Index(packager, `cp "$PAYLOAD/$BINNAME.exe" "$portable_staging/versions/$version_label/$BINNAME.exe"`)
118 bundle := strings.Index(packager, `installer_bundle="$DESKTOP/build/windows/installer-signing-bundle"`)
119 if copyMain < 0 || makeNSIS < 0 || portable < 0 || bundle < 0 {
120 t.Fatal("Windows packager is missing the signed-payload packaging stages")
121 }
122 if !(copyMain < makeNSIS && makeNSIS < portable && portable < bundle) {
123 t.Fatalf("Windows package order must be payload copy -> NSIS -> portable -> signing bundle (copy=%d nsis=%d portable=%d bundle=%d)", copyMain, makeNSIS, portable, bundle)
124 }
125 for _, want := range []string{
126 `cp "$PAYLOAD/$GUARDNAME.exe" "$INSTALLER_DIR/$GUARDNAME.exe"`,
127 `cp "$PAYLOAD/$LAUNCHERNAME.exe" "$INSTALLER_DIR/$LAUNCHERNAME.exe"`,
128 `cp "$PAYLOAD/$UPDATE_HELPER" "$INSTALLER_DIR/$UPDATE_HELPER"`,
129 `cp "$PAYLOAD/$WINDOWS_CLINAME.exe" "$INSTALLER_DIR/$WINDOWS_CLINAME.exe"`,
130 `rm -f -- "$INSTALLER_DIR/$PAYLOAD_MANIFEST" "$INSTALLER_DIR/$PAYLOAD_SIGNATURE"`,
131 `cp "$PAYLOAD/$PAYLOAD_MANIFEST" "$INSTALLER_DIR/$PAYLOAD_MANIFEST"`,
132 `cp "$PAYLOAD/$PAYLOAD_SIGNATURE" "$INSTALLER_DIR/$PAYLOAD_SIGNATURE"`,
133 `REASONIX_REQUIRE_PAYLOAD_MANIFEST`,
134 `"-DARG_REASONIX_SIGNED_UNINSTALLER=${uninstaller_path}"`,
135 `cp "$PAYLOAD/$LAUNCHERNAME.exe" "$portable_staging/$APPNAME.exe"`,
136 `"$ROOT/scripts/verify-windows-portable.sh" "$portable_staging"`,
137 } {
138 if !strings.Contains(packager, want) {
139 t.Errorf("Windows packager is missing payload contract %q", want)
140 }
141 }
142
143 verifier := readTestFile(t, "../scripts/verify-windows-authenticode.ps1")
144 for _, want := range []string{
145 "Get-AuthenticodeSignature",
146 "$signature.SignerCertificate",
147 "$signature.Status -ne \"Valid\"",
148 "Expand-Archive",
149 `Get-ChildItem -LiteralPath $extractRoot -Recurse -File -Filter "*.exe"`,
150 `$activeDir.Replace("\", "/") -ne "versions/$activeVersion"`,
151 `Portable = (Join-Path $activeDir "reasonix-desktop.exe")`,
152 `Portable = "reasonix-desktop.exe"`,
153 "Portable archive must contain exactly 6 executables",
154 "Get-FileHash -Algorithm SHA256",
155 } {
156 if !strings.Contains(verifier, want) {
157 t.Errorf("Windows Authenticode verifier is missing %q", want)
158 }
159 }
160
161 completer := readTestFile(t, "../scripts/complete-signpath-request.ps1")
162 for _, want := range []string{
163 `$request.signingPolicySlug -ne $ExpectedSigningPolicySlug`,
164 `$status.status -eq "WaitingForApproval"`,
165 `"$requestBaseUrl/Approve"`,
166 `"$requestBaseUrl/Status"`,
167 `"$requestBaseUrl/SignedArtifact"`,
168 `$status.status -ne "Completed"`,
169 `[switch]$WaitForExternalApproval`,
170 `if ($WaitForExternalApproval)`,
171 `Waiting for an authorized SignPath user to approve request`,
172 `OutputArtifactDirectory must resolve inside GITHUB_WORKSPACE`,
173 `[string]$ApiUrl = "https://app.signpath.io/api"`,
174 `Expand-Archive`,
175 } {
176 if !strings.Contains(completer, want) {
177 t.Errorf("SignPath request completer is missing %q", want)
178 }
179 }
180 }
181
182 func TestWindowsPackagerRejectsMissingOrPartialRequiredPayloadManifest(t *testing.T) {
183 for _, tc := range []struct {
184 name string
185 manifest bool
186 signature bool
187 want string
188 }{
189 {name: "missing", want: "signed Windows packaging requires"},
190 {name: "manifest only", manifest: true, want: "must be provided together"},
191 {name: "signature only", signature: true, want: "must be provided together"},
192 } {
193 t.Run(tc.name, func(t *testing.T) {
194 payload := t.TempDir()
195 for _, name := range []string{
196 "reasonix-desktop.exe",
197 "reasonix-guard.exe",
198 "reasonix-launcher.exe",
199 "reasonix-update-helper.exe",
200 "reasonix-cli.exe",
201 "reasonix-uninstall.exe",
202 } {
203 if err := os.WriteFile(filepath.Join(payload, name), []byte(name), 0o600); err != nil {
204 t.Fatal(err)
205 }
206 }
207 if tc.manifest {
208 if err := os.WriteFile(filepath.Join(payload, "reasonix-payload.json"), []byte("{}"), 0o600); err != nil {
209 t.Fatal(err)
210 }
211 }
212 if tc.signature {
213 if err := os.WriteFile(filepath.Join(payload, "reasonix-payload.json.minisig"), []byte("sig"), 0o600); err != nil {
214 t.Fatal(err)
215 }
216 }
217 cmd := exec.Command("bash", "../scripts/package-windows-desktop.sh", "amd64", payload)
218 cmd.Env = append(os.Environ(), "REASONIX_REQUIRE_PAYLOAD_MANIFEST=1")
219 output, err := cmd.CombinedOutput()
220 if err == nil || !strings.Contains(string(output), tc.want) {
221 t.Fatalf("packager error = %v, output = %q, want %q", err, output, tc.want)
222 }
223 })
224 }
225 }
226
227 func TestProductionSigningRunsOnlyFromProtectedControlPlane(t *testing.T) {
228 stable := readTestFile(t, "../.github/workflows/release-stable.yml")
229 desktop := readTestFile(t, "../.github/workflows/release-desktop.yml")
230 if strings.Contains(stable, "\n push:\n") || strings.Contains(desktop, "\n push:\n") {
231 t.Fatal("production workflows must not run directly with a tag-shaped SignPath origin")
232 }
233 for _, want := range []string{
234 `ALLOW_STABLE_RECOVERY: ${{ inputs.allow_recovery }}`,
235 `allow_recovery: 'false'`,
236 `signing_preflight: true`,
237 `signing_preflight_verified: true`,
238 `needs: [authorize, signpath-preflight]`,
239 } {
240 if !strings.Contains(stable+"\n"+readTestFile(t, "../.github/workflows/release-stable-trigger.yml"), want) {
241 t.Errorf("stable relay is missing normal-release recovery guard %q", want)
242 }
243 }
244
245 for _, path := range []string{
246 "../.github/workflows/release-stable-trigger.yml",
247 } {
248 relay := readTestFile(t, path)
249 for _, want := range []string{
250 `actions: write`,
251 `CONTROL_PLANE_REF: ${{ github.event.repository.default_branch }}`,
252 `process.env.CONTROL_PLANE_REF !== 'main-v2'`,
253 `createWorkflowDispatch`,
254 `ref: process.env.CONTROL_PLANE_REF`,
255 } {
256 if !strings.Contains(relay, want) {
257 t.Errorf("%s is missing protected control-plane contract %q", path, want)
258 }
259 }
260 }
261
262 for _, path := range []string{
263 "../.github/workflows/release-preview.yml",
264 "../.github/workflows/release-cli-trigger.yml",
265 "../.github/workflows/release-desktop-trigger.yml",
266 } {
267 if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) {
268 t.Errorf("retired public prerelease workflow %s still exists or cannot be checked: %v", path, err)
269 }
270 }
271 }
272
273 func TestSignPathConfigurationsCoverExactWindowsPayload(t *testing.T) {
274 expected := map[string]bool{
275 "reasonix-desktop.exe": true,
276 "reasonix-guard.exe": true,
277 "reasonix-launcher.exe": true,
278 "reasonix-update-helper.exe": true,
279 "reasonix-cli.exe": true,
280 "reasonix-uninstall.exe": true,
281 }
282
283 payload := parseSignPathConfiguration(t, "windows-payload.xml")
284 if len(payload.Zip.Files) != len(expected) {
285 t.Fatalf("windows-payload.xml files = %d, want %d", len(payload.Zip.Files), len(expected))
286 }
287 for _, file := range payload.Zip.Files {
288 if !expected[file.Path] {
289 t.Errorf("windows-payload.xml contains unexpected path %q", file.Path)
290 }
291 if file.Sign == nil || file.Verify != nil {
292 t.Errorf("windows-payload.xml %q must sign, not verify", file.Path)
293 }
294 }
295
296 installer := parseSignPathConfiguration(t, "windows-installer-v2.xml")
297 if len(installer.Zip.Files) != len(expected)+1 {
298 t.Fatalf("windows-installer.xml files = %d, want %d", len(installer.Zip.Files), len(expected)+1)
299 }
300 verified := 0
301 signedInstaller := 0
302 for _, file := range installer.Zip.Files {
303 switch {
304 case file.Path == "*installer*.exe":
305 if file.Sign == nil || file.Verify != nil {
306 t.Error("windows-installer.xml must sign the outer installer")
307 }
308 signedInstaller++
309 case expected[file.Path]:
310 if file.Verify == nil || file.Sign != nil {
311 t.Errorf("windows-installer.xml %q must verify, not re-sign", file.Path)
312 }
313 verified++
314 default:
315 t.Errorf("windows-installer.xml contains unexpected path %q", file.Path)
316 }
317 }
318 if signedInstaller != 1 || verified != len(expected) {
319 t.Fatalf("windows-installer.xml signed installers=%d verified payload=%d", signedInstaller, verified)
320 }
321
322 testInstaller := parseSignPathConfiguration(t, "windows-installer-test-v2.xml")
323 if len(testInstaller.Zip.Files) != 1 {
324 t.Fatalf("windows-installer-test-v2.xml files = %d, want 1", len(testInstaller.Zip.Files))
325 }
326 file := testInstaller.Zip.Files[0]
327 if file.Path != "*installer*.exe" || file.Sign == nil || file.Verify != nil {
328 t.Fatal("windows-installer-test-v2.xml must only sign the outer installer")
329 }
330 }
331
331 lines GO