ci(release): require trusted Windows signing
This commit is contained in:
parent
5205dcdfa9
commit
3e9e04f68c
5 changed files with 279 additions and 4 deletions
28
.github/workflows/release.yml
vendored
28
.github/workflows/release.yml
vendored
|
|
@ -110,13 +110,39 @@ jobs:
|
||||||
npm run build --workspace=@d3ro/desktop
|
npm run build --workspace=@d3ro/desktop
|
||||||
|
|
||||||
- name: Package with Electron Builder (NSIS x64)
|
- name: Package with Electron Builder (NSIS x64)
|
||||||
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
cd apps/desktop
|
$ErrorActionPreference = 'Stop'
|
||||||
|
if ([string]::IsNullOrWhiteSpace($env:CSC_LINK)) {
|
||||||
|
throw 'WIN_CSC_LINK must contain the production code-signing PFX path, URL, or base64 payload.'
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($env:CSC_KEY_PASSWORD)) {
|
||||||
|
throw 'WIN_CSC_KEY_PASSWORD is required.'
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($env:WIN_CSC_EXPECTED_SIGNER_SUBJECT)) {
|
||||||
|
throw 'WIN_CSC_EXPECTED_SIGNER_SUBJECT is required.'
|
||||||
|
}
|
||||||
|
if ($env:WIN_CSC_EXPECTED_SIGNER_SUBJECT -match '(?i)Everything2EverythingDev') {
|
||||||
|
throw 'The local Everything2EverythingDev certificate is not a production signing identity.'
|
||||||
|
}
|
||||||
|
$releaseVersion = node -p "require('./release/product-version.json').version"
|
||||||
|
Push-Location apps/desktop
|
||||||
|
try {
|
||||||
npx electron-builder --win --x64 --config electron-builder.yml
|
npx electron-builder --win --x64 --config electron-builder.yml
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "electron-builder failed with exit code $LASTEXITCODE." }
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
|
& scripts/ci/verify-windows-release-artifact.ps1 `
|
||||||
|
-ExpectedVersion $releaseVersion `
|
||||||
|
-ExpectedSignerSubject $env:WIN_CSC_EXPECTED_SIGNER_SUBJECT `
|
||||||
|
-ReleaseDirectory "apps/desktop/release/$releaseVersion"
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
|
CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
|
CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
|
||||||
|
WIN_CSC_EXPECTED_SIGNER_SUBJECT: ${{ secrets.WIN_CSC_EXPECTED_SIGNER_SUBJECT }}
|
||||||
|
|
||||||
- name: Upload Windows Build Artifacts
|
- name: Upload Windows Build Artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|
|
||||||
|
|
@ -206,10 +206,32 @@ package-windows:
|
||||||
before_script:
|
before_script:
|
||||||
- npm ci
|
- npm ci
|
||||||
script:
|
script:
|
||||||
|
- |
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
if ([string]::IsNullOrWhiteSpace($env:WIN_CSC_PFX_FILE) -or -not (Test-Path -LiteralPath $env:WIN_CSC_PFX_FILE -PathType Leaf)) {
|
||||||
|
throw 'WIN_CSC_PFX_FILE must be a protected GitLab file variable containing the production code-signing PFX.'
|
||||||
|
}
|
||||||
|
if ((Get-Item -LiteralPath $env:WIN_CSC_PFX_FILE).Length -le 0) {
|
||||||
|
throw 'WIN_CSC_PFX_FILE is empty.'
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($env:WIN_CSC_KEY_PASSWORD)) {
|
||||||
|
throw 'WIN_CSC_KEY_PASSWORD must be a protected, masked GitLab variable.'
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($env:WIN_CSC_EXPECTED_SIGNER_SUBJECT)) {
|
||||||
|
throw 'WIN_CSC_EXPECTED_SIGNER_SUBJECT must be a protected GitLab variable.'
|
||||||
|
}
|
||||||
|
if ($env:WIN_CSC_EXPECTED_SIGNER_SUBJECT -match '(?i)Everything2EverythingDev') {
|
||||||
|
throw 'The local Everything2EverythingDev certificate is not a production signing identity.'
|
||||||
|
}
|
||||||
|
$env:CSC_LINK = (Resolve-Path -LiteralPath $env:WIN_CSC_PFX_FILE).Path
|
||||||
|
$env:CSC_KEY_PASSWORD = $env:WIN_CSC_KEY_PASSWORD
|
||||||
- node scripts/ci/sync-version.mjs --check --tag "$CI_COMMIT_TAG"
|
- node scripts/ci/sync-version.mjs --check --tag "$CI_COMMIT_TAG"
|
||||||
- npm run build --workspace=@d3ro/desktop
|
- npm run build --workspace=@d3ro/desktop
|
||||||
- cd apps/desktop
|
- cd apps/desktop
|
||||||
- npx electron-builder --win --x64 --config electron-builder.yml
|
- npx electron-builder --win --x64 --config electron-builder.yml
|
||||||
|
- cd ../..
|
||||||
|
- $releaseVersion = node -p "require('./release/product-version.json').version"
|
||||||
|
- '& scripts/ci/verify-windows-release-artifact.ps1 -ExpectedVersion $releaseVersion -ExpectedSignerSubject $env:WIN_CSC_EXPECTED_SIGNER_SUBJECT -ReleaseDirectory "apps/desktop/release/$releaseVersion"'
|
||||||
artifacts:
|
artifacts:
|
||||||
name: "d3ro-voice-windows-$CI_COMMIT_TAG"
|
name: "d3ro-voice-windows-$CI_COMMIT_TAG"
|
||||||
paths:
|
paths:
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- Removed synthetic success fallbacks from protected STT and AI paths; provider and configuration failures now return explicit failures without creating fabricated user data.
|
- Removed synthetic success fallbacks from protected STT and AI paths; provider and configuration failures now return explicit failures without creating fabricated user data.
|
||||||
- Strengthened generated-document reporting so only the owner can report an existing document backed by immutable generation audit evidence.
|
- Strengthened generated-document reporting so only the owner can report an existing document backed by immutable generation audit evidence.
|
||||||
- Removed the repository-exposed desktop license private key and prefix-only paid-tier activation; production now accepts only Ed25519 licenses signed by the rotated external key.
|
- Removed the repository-exposed desktop license private key and prefix-only paid-tier activation; production now accepts only Ed25519 licenses signed by the rotated external key.
|
||||||
|
- Made Windows stable packaging fail closed unless an external public-trust code-signing PFX is provided and both the NSIS installer and unpacked app pass exact-version, updater SHA-512, Authenticode `Valid`, and expected signer-subject verification; local self-signed development certificates are rejected.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Corrected realtime Edge Function model routing and type checks for team and enterprise tiers.
|
- Corrected realtime Edge Function model routing and type checks for team and enterprise tiers.
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
| Android upload key | alias `d3ro-upload-20260821`, cert SHA-256 `4F:AC:69:24:...:15:2B:54` | external PKCS12·user-only ACL·Credential Manager·private-key readback GREEN; CI secret·복구 백업·AAB signer 대조 대기 |
|
| Android upload key | alias `d3ro-upload-20260821`, cert SHA-256 `4F:AC:69:24:...:15:2B:54` | external PKCS12·user-only ACL·Credential Manager·private-key readback GREEN; CI secret·복구 백업·AAB signer 대조 대기 |
|
||||||
| release evidence | Ed25519 public `release/mobile-release-evidence-public.pem`, keyId `2797d3e6...4a890b7f` | external private key ACL·roundtrip GREEN; CI private-key secret·복구 백업 대기 |
|
| release evidence | Ed25519 public `release/mobile-release-evidence-public.pem`, keyId `2797d3e6...4a890b7f` | external private key ACL·roundtrip GREEN; CI private-key secret·복구 백업 대기 |
|
||||||
| desktop offline license | Ed25519 public `apps/desktop/resources/license/production-public.pem`, keyId `5c52b765...81a887f` | 새 전용 keypair·external private ACL·roundtrip·desktop production build GREEN; admin `ADMIN_LICENSE_PRIVATE_KEY` secret 주입 대기 |
|
| desktop offline license | Ed25519 public `apps/desktop/resources/license/production-public.pem`, keyId `5c52b765...81a887f` | 새 전용 keypair·external private ACL·roundtrip·desktop production build GREEN; admin `ADMIN_LICENSE_PRIVATE_KEY` secret 주입 대기 |
|
||||||
|
| Windows Authenticode | external public-trust code-signing certificate | 현재 local `1.1.0` installer·unpacked app은 `NotSigned`; production PFX·CI secret·signed artifact GREEN 전까지 게시 금지 |
|
||||||
| Firebase | Console `u/0`, `u/1` 모두 D3RO project 없음 | 사용자 승인 후 project·Android app 생성 필요 |
|
| Firebase | Console `u/0`, `u/1` 모두 D3RO project 없음 | 사용자 승인 후 project·Android app 생성 필요 |
|
||||||
| AdMob | app `ca-app-pub-1039714767792854~6427959892`; banner `/9840591290`; rewarded `/2255790918` | SSOT 확정. `검토 필요`·`광고 게재 제한`·store 미연결·결제 프로필 미완료 |
|
| AdMob | app `ca-app-pub-1039714767792854~6427959892`; banner `/9840591290`; rewarded `/2255790918` | SSOT 확정. `검토 필요`·`광고 게재 제한`·store 미연결·결제 프로필 미완료 |
|
||||||
| updater feed | `https://gitlab.twentyoz.kr:8443/api/v4/projects/1172/packages/generic/d3ro-voice/latest` | public `latest.yml`은 아직 `0.2.1-alpha`; `1.1.0` 미배포 |
|
| updater feed | `https://gitlab.twentyoz.kr:8443/api/v4/projects/1172/packages/generic/d3ro-voice/latest` | public `latest.yml`은 아직 `0.2.1-alpha`; `1.1.0` 미배포 |
|
||||||
|
|
@ -38,6 +39,8 @@ authoritative release commit
|
||||||
- 같은 gate는 desktop license public key가 Ed25519이고 `release/product-version.json`의 `desktopLicensePublicKeyId`와 일치하는지 검증한다. `electron.vite.config.ts`는 이 파일을 직접 읽으므로 누락·손상된 키로는 build가 시작되지 않는다.
|
- 같은 gate는 desktop license public key가 Ed25519이고 `release/product-version.json`의 `desktopLicensePublicKeyId`와 일치하는지 검증한다. `electron.vite.config.ts`는 이 파일을 직접 읽으므로 누락·손상된 키로는 build가 시작되지 않는다.
|
||||||
- `scripts/ci/publish-gitlab-release.mjs`는 버전별 패키지를 먼저 올리고, `latest` 파일에서 설치 자산 참조를 검증한 후 update metadata를 마지막에 게시한다.
|
- `scripts/ci/publish-gitlab-release.mjs`는 버전별 패키지를 먼저 올리고, `latest` 파일에서 설치 자산 참조를 검증한 후 update metadata를 마지막에 게시한다.
|
||||||
- Windows installer와 `latest.yml`은 필수다. macOS 산출물이 없는 Windows-only release를 의도했다면 그 판단을 release record에 남긴다.
|
- Windows installer와 `latest.yml`은 필수다. macOS 산출물이 없는 Windows-only release를 의도했다면 그 판단을 release record에 남긴다.
|
||||||
|
- `package-windows`는 external public-trust code-signing PFX를 protected file variable `WIN_CSC_PFX_FILE`로, 암호와 exact certificate subject를 protected `WIN_CSC_KEY_PASSWORD`, `WIN_CSC_EXPECTED_SIGNER_SUBJECT`로 받아야 한다. GitHub Release도 `WIN_CSC_LINK`, `WIN_CSC_KEY_PASSWORD`, `WIN_CSC_EXPECTED_SIGNER_SUBJECT`가 모두 없으면 실패한다.
|
||||||
|
- `scripts/ci/verify-windows-release-artifact.ps1`는 installer와 unpacked app의 Authenticode `Valid`, exact signer subject, non-self-signed code-signing EKU, PE version, `latest.yml` path/size/SHA-512를 검증한다. 로컬 self-signed `Everything2EverythingDev`는 production 신뢰 인증서가 아니며 gate에서 명시적으로 거부한다.
|
||||||
|
|
||||||
## 자동 업데이트 계약
|
## 자동 업데이트 계약
|
||||||
|
|
||||||
|
|
@ -54,7 +57,8 @@ https://gitlab.twentyoz.kr:8443/api/v4/projects/1172/packages/generic/d3ro-voice
|
||||||
3. `latest.yml` URL/path가 같은 `latest` 경로의 실제 installer를 참조한다.
|
3. `latest.yml` URL/path가 같은 `latest` 경로의 실제 installer를 참조한다.
|
||||||
4. installer 파일명에 공백이 없다: `D3RO-Voice-Setup-<version>-x64.exe`.
|
4. installer 파일명에 공백이 없다: `D3RO-Voice-Setup-<version>-x64.exe`.
|
||||||
5. metadata SHA-512와 다운로드한 installer가 일치한다.
|
5. metadata SHA-512와 다운로드한 installer가 일치한다.
|
||||||
6. 이전 실제 설치본이 feed를 탐지하고, 다운로드·재시작·버전 상승을 끝까지 완료한다.
|
6. installer와 `win-unpacked/D3RO Voice.exe` 모두 external public-trust certificate의 Authenticode `Valid`이고, signer subject가 protected CI identity와 정확히 일치한다.
|
||||||
|
7. 이전 실제 설치본이 feed를 탐지하고, 다운로드·재시작·버전 상승을 끝까지 완료한다.
|
||||||
|
|
||||||
2026-08-29 live `latest.yml`의 버전은 `0.2.1-alpha`다. 이는 updater endpoint가 응답한다는 증거일 뿐 `1.1.0` 게시 증거가 아니다.
|
2026-08-29 live `latest.yml`의 버전은 `0.2.1-alpha`다. 이는 updater endpoint가 응답한다는 증거일 뿐 `1.1.0` 게시 증거가 아니다.
|
||||||
|
|
||||||
|
|
@ -63,7 +67,7 @@ https://gitlab.twentyoz.kr:8443/api/v4/projects/1172/packages/generic/d3ro-voice
|
||||||
1. `release/product-version.json`의 version/build 값과 모든 버전 면을 `npm run version:check`로 대조한다.
|
1. `release/product-version.json`의 version/build 값과 모든 버전 면을 `npm run version:check`로 대조한다.
|
||||||
2. `CHANGELOG.md` `## [1.1.0] - 2026-08-29` 섹션을 사용자 변경점 중심으로 확정한다. publisher는 이 섹션이 없으면 실패해야 한다.
|
2. `CHANGELOG.md` `## [1.1.0] - 2026-08-29` 섹션을 사용자 변경점 중심으로 확정한다. publisher는 이 섹션이 없으면 실패해야 한다.
|
||||||
3. dirty/untracked 작업을 임의로 reset·clean하지 말고, release 범위만 검토 가능한 authoritative commit으로 보존한다.
|
3. dirty/untracked 작업을 임의로 reset·clean하지 말고, release 범위만 검토 가능한 authoritative commit으로 보존한다.
|
||||||
4. 같은 commit에서 lint, typecheck, test, build, release metadata·security·artifact gate를 전부 GREEN으로 만든다.
|
4. 같은 commit에서 lint, typecheck, test, build, release metadata·security·artifact gate를 전부 GREEN으로 만든다. Windows는 production Authenticode PFX를 주입한 CI build와 `verify-windows-release-artifact.ps1` GREEN이 필수다.
|
||||||
5. desktop offline license를 제공한다면 external private key를 admin의 `ADMIN_LICENSE_PRIVATE_KEY` secret로 주입하고, 저장소 public key와 sign/verify roundtrip 및 발급 감사 로그를 확인한다.
|
5. desktop offline license를 제공한다면 external private key를 admin의 `ADMIN_LICENSE_PRIVATE_KEY` secret로 주입하고, 저장소 public key와 sign/verify roundtrip 및 발급 감사 로그를 확인한다.
|
||||||
6. 이전 버전보다 높은 태그 `v1.1.0`을 생성해 push한다. 태그는 게이트를 시작하는 후속 단계지 검증을 대체하지 않는다.
|
6. 이전 버전보다 높은 태그 `v1.1.0`을 생성해 push한다. 태그는 게이트를 시작하는 후속 단계지 검증을 대체하지 않는다.
|
||||||
7. GitLab에서 package-windows, package-macos, publish-release와 의도한 mobile job 상태를 모두 확인한다. pending/stuck/skipped를 GREEN으로 기록하지 않는다.
|
7. GitLab에서 package-windows, package-macos, publish-release와 의도한 mobile job 상태를 모두 확인한다. pending/stuck/skipped를 GREEN으로 기록하지 않는다.
|
||||||
|
|
@ -96,6 +100,7 @@ Desktop GitLab Release를 게시해도 Android production 출시가 자동으로
|
||||||
- `apps/desktop/resources/license/production-public.pem` — desktop offline license public key SSOT
|
- `apps/desktop/resources/license/production-public.pem` — desktop offline license public key SSOT
|
||||||
- `scripts/ci/sync-version.mjs` — 버전 면 동기화·검증
|
- `scripts/ci/sync-version.mjs` — 버전 면 동기화·검증
|
||||||
- `scripts/ci/verify-release-metadata.mjs` — release metadata 자가 검증
|
- `scripts/ci/verify-release-metadata.mjs` — release metadata 자가 검증
|
||||||
|
- `scripts/ci/verify-windows-release-artifact.ps1` — Windows version·updater metadata·Authenticode gate
|
||||||
- `scripts/ci/publish-gitlab-release.mjs` — registry·Release·updater feed publisher
|
- `scripts/ci/publish-gitlab-release.mjs` — registry·Release·updater feed publisher
|
||||||
- `apps/desktop/src/main/update-feed.ts` — runtime updater URL SSOT
|
- `apps/desktop/src/main/update-feed.ts` — runtime updater URL SSOT
|
||||||
- `apps/desktop/electron-builder.yml` — builder publish URL·artifact contract
|
- `apps/desktop/electron-builder.yml` — builder publish URL·artifact contract
|
||||||
|
|
|
||||||
221
scripts/ci/verify-windows-release-artifact.ps1
Normal file
221
scripts/ci/verify-windows-release-artifact.ps1
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
#Requires -Version 5.1
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[ValidatePattern('^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$')]
|
||||||
|
[string]$ExpectedVersion,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[ValidateNotNullOrEmpty()]
|
||||||
|
[string]$ExpectedSignerSubject,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[string]$ReleaseDirectory
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Fail-Verification {
|
||||||
|
param([Parameter(Mandatory = $true)][string]$Reason)
|
||||||
|
throw "windows_release_verification_failed:$Reason"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RequiredYamlValue {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$Text,
|
||||||
|
[Parameter(Mandatory = $true)][string]$Pattern,
|
||||||
|
[Parameter(Mandatory = $true)][string]$Name
|
||||||
|
)
|
||||||
|
|
||||||
|
$matches = [regex]::Matches($Text, $Pattern)
|
||||||
|
if ($matches.Count -ne 1) {
|
||||||
|
Fail-Verification "latest_yml_${Name}_count_$($matches.Count)"
|
||||||
|
}
|
||||||
|
|
||||||
|
return $matches[0].Groups[1].Value.Trim().Trim('"').Trim("'")
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-Sha512Base64 {
|
||||||
|
param([Parameter(Mandatory = $true)][string]$Path)
|
||||||
|
|
||||||
|
$stream = [System.IO.File]::OpenRead($Path)
|
||||||
|
$sha512 = [System.Security.Cryptography.SHA512]::Create()
|
||||||
|
try {
|
||||||
|
return [Convert]::ToBase64String($sha512.ComputeHash($stream))
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$sha512.Dispose()
|
||||||
|
$stream.Dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-PeVersion {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$Path,
|
||||||
|
[Parameter(Mandatory = $true)][string]$Label,
|
||||||
|
[Parameter(Mandatory = $true)][version]$Expected
|
||||||
|
)
|
||||||
|
|
||||||
|
$versionInfo = (Get-Item -LiteralPath $Path).VersionInfo
|
||||||
|
foreach ($entry in @(
|
||||||
|
@{ Name = 'file_version'; Value = $versionInfo.FileVersion },
|
||||||
|
@{ Name = 'product_version'; Value = $versionInfo.ProductVersion }
|
||||||
|
)) {
|
||||||
|
$value = [string]$entry.Value
|
||||||
|
if ($value -notmatch '^\d+\.\d+\.\d+(?:\.0)?$') {
|
||||||
|
Fail-Verification "${Label}_$($entry.Name)_invalid"
|
||||||
|
}
|
||||||
|
|
||||||
|
$actual = [version]$value
|
||||||
|
if ($actual.Major -ne $Expected.Major -or
|
||||||
|
$actual.Minor -ne $Expected.Minor -or
|
||||||
|
$actual.Build -ne $Expected.Build -or
|
||||||
|
($actual.Revision -notin @(-1, 0))) {
|
||||||
|
Fail-Verification "${Label}_$($entry.Name)_mismatch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($versionInfo.ProductName -cne 'D3RO Voice') {
|
||||||
|
Fail-Verification "${Label}_product_name_mismatch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ProductionSignature {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$Path,
|
||||||
|
[Parameter(Mandatory = $true)][string]$Label,
|
||||||
|
[Parameter(Mandatory = $true)][string]$ExpectedSubject
|
||||||
|
)
|
||||||
|
|
||||||
|
$signature = Get-AuthenticodeSignature -LiteralPath $Path
|
||||||
|
if ($signature.Status -ne [System.Management.Automation.SignatureStatus]::Valid) {
|
||||||
|
Fail-Verification "${Label}_authenticode_$($signature.Status)"
|
||||||
|
}
|
||||||
|
|
||||||
|
$certificate = $signature.SignerCertificate
|
||||||
|
if ($null -eq $certificate) {
|
||||||
|
Fail-Verification "${Label}_signer_certificate_missing"
|
||||||
|
}
|
||||||
|
|
||||||
|
$actualSubject = $certificate.Subject.Trim()
|
||||||
|
if (-not [string]::Equals($actualSubject, $ExpectedSubject, [StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
Fail-Verification "${Label}_signer_subject_mismatch"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::Equals($certificate.Subject, $certificate.Issuer, [StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
Fail-Verification "${Label}_self_signed_certificate"
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = [DateTime]::UtcNow
|
||||||
|
if ($certificate.NotBefore.ToUniversalTime() -gt $now -or $certificate.NotAfter.ToUniversalTime() -le $now) {
|
||||||
|
Fail-Verification "${Label}_signer_certificate_not_current"
|
||||||
|
}
|
||||||
|
|
||||||
|
$codeSigningOid = '1.3.6.1.5.5.7.3.3'
|
||||||
|
$ekuOids = @()
|
||||||
|
foreach ($extension in $certificate.Extensions) {
|
||||||
|
if ($extension -is [System.Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension]) {
|
||||||
|
foreach ($usage in $extension.EnhancedKeyUsages) {
|
||||||
|
$ekuOids += $usage.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($ekuOids -notcontains $codeSigningOid) {
|
||||||
|
Fail-Verification "${Label}_code_signing_eku_missing"
|
||||||
|
}
|
||||||
|
|
||||||
|
return $certificate
|
||||||
|
}
|
||||||
|
|
||||||
|
$expectedSubject = $ExpectedSignerSubject.Trim()
|
||||||
|
if ([string]::IsNullOrWhiteSpace($expectedSubject)) {
|
||||||
|
Fail-Verification 'expected_signer_subject_empty'
|
||||||
|
}
|
||||||
|
if ($expectedSubject -match '(?i)Everything2EverythingDev') {
|
||||||
|
Fail-Verification 'development_signer_subject_forbidden'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ReleaseDirectory)) {
|
||||||
|
$ReleaseDirectory = Join-Path $PSScriptRoot "../../apps/desktop/release/$ExpectedVersion"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $ReleaseDirectory -PathType Container)) {
|
||||||
|
Fail-Verification 'release_directory_missing'
|
||||||
|
}
|
||||||
|
$releasePath = (Resolve-Path -LiteralPath $ReleaseDirectory).Path
|
||||||
|
if ((Split-Path -Leaf $releasePath) -cne $ExpectedVersion) {
|
||||||
|
Fail-Verification 'release_directory_version_mismatch'
|
||||||
|
}
|
||||||
|
|
||||||
|
$installerName = "D3RO-Voice-Setup-$ExpectedVersion-x64.exe"
|
||||||
|
$installerPath = Join-Path $releasePath $installerName
|
||||||
|
$latestPath = Join-Path $releasePath 'latest.yml'
|
||||||
|
$appPath = Join-Path $releasePath 'win-unpacked/D3RO Voice.exe'
|
||||||
|
|
||||||
|
foreach ($requiredFile in @($installerPath, $latestPath, $appPath)) {
|
||||||
|
if (-not (Test-Path -LiteralPath $requiredFile -PathType Leaf)) {
|
||||||
|
Fail-Verification "required_file_missing_$(Split-Path -Leaf $requiredFile)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$installers = @(Get-ChildItem -LiteralPath $releasePath -File -Filter 'D3RO-Voice-Setup-*-x64.exe')
|
||||||
|
if ($installers.Count -ne 1 -or $installers[0].Name -cne $installerName) {
|
||||||
|
Fail-Verification 'installer_set_not_exact'
|
||||||
|
}
|
||||||
|
|
||||||
|
$latestText = [System.IO.File]::ReadAllText($latestPath)
|
||||||
|
$metadataVersion = Get-RequiredYamlValue $latestText '(?m)^version:\s*([^\r\n#]+?)\s*$' 'version'
|
||||||
|
$metadataUrl = Get-RequiredYamlValue $latestText '(?m)^\s{2}-\s+url:\s*([^\r\n#]+?)\s*$' 'file_url'
|
||||||
|
$metadataFileSha512 = Get-RequiredYamlValue $latestText '(?m)^\s{4}sha512:\s*([^\r\n#]+?)\s*$' 'file_sha512'
|
||||||
|
$metadataSizeText = Get-RequiredYamlValue $latestText '(?m)^\s{4}size:\s*([^\r\n#]+?)\s*$' 'file_size'
|
||||||
|
$metadataPath = Get-RequiredYamlValue $latestText '(?m)^path:\s*([^\r\n#]+?)\s*$' 'path'
|
||||||
|
$metadataSha512 = Get-RequiredYamlValue $latestText '(?m)^sha512:\s*([^\r\n#]+?)\s*$' 'sha512'
|
||||||
|
|
||||||
|
if ($metadataVersion -cne $ExpectedVersion) {
|
||||||
|
Fail-Verification 'latest_yml_version_mismatch'
|
||||||
|
}
|
||||||
|
if ($metadataUrl -cne $installerName -or $metadataPath -cne $installerName) {
|
||||||
|
Fail-Verification 'latest_yml_installer_reference_mismatch'
|
||||||
|
}
|
||||||
|
|
||||||
|
[long]$metadataSize = 0
|
||||||
|
if (-not [long]::TryParse($metadataSizeText, [ref]$metadataSize) -or $metadataSize -le 0) {
|
||||||
|
Fail-Verification 'latest_yml_size_invalid'
|
||||||
|
}
|
||||||
|
$installerSize = (Get-Item -LiteralPath $installerPath).Length
|
||||||
|
if ($metadataSize -ne $installerSize) {
|
||||||
|
Fail-Verification 'latest_yml_size_mismatch'
|
||||||
|
}
|
||||||
|
|
||||||
|
$installerSha512 = Get-Sha512Base64 $installerPath
|
||||||
|
if ($metadataFileSha512 -cne $installerSha512 -or $metadataSha512 -cne $installerSha512) {
|
||||||
|
Fail-Verification 'latest_yml_sha512_mismatch'
|
||||||
|
}
|
||||||
|
|
||||||
|
$expectedPeVersion = [version]"$ExpectedVersion.0"
|
||||||
|
Assert-PeVersion $installerPath 'installer' $expectedPeVersion
|
||||||
|
Assert-PeVersion $appPath 'unpacked_app' $expectedPeVersion
|
||||||
|
|
||||||
|
$installerCertificate = Assert-ProductionSignature $installerPath 'installer' $expectedSubject
|
||||||
|
$appCertificate = Assert-ProductionSignature $appPath 'unpacked_app' $expectedSubject
|
||||||
|
if ($installerCertificate.Thumbprint -cne $appCertificate.Thumbprint) {
|
||||||
|
Fail-Verification 'signer_thumbprint_mismatch'
|
||||||
|
}
|
||||||
|
|
||||||
|
$evidence = [ordered]@{
|
||||||
|
schemaVersion = 1
|
||||||
|
version = $ExpectedVersion
|
||||||
|
installer = $installerName
|
||||||
|
installerSize = $installerSize
|
||||||
|
installerSha512 = $installerSha512
|
||||||
|
unpackedApp = 'win-unpacked/D3RO Voice.exe'
|
||||||
|
authenticodeStatus = 'Valid'
|
||||||
|
signerSubject = $installerCertificate.Subject
|
||||||
|
signerIssuer = $installerCertificate.Issuer
|
||||||
|
signerThumbprint = $installerCertificate.Thumbprint
|
||||||
|
verifiedAtUtc = [DateTime]::UtcNow.ToString('o')
|
||||||
|
}
|
||||||
|
|
||||||
|
$evidence | ConvertTo-Json -Depth 3
|
||||||
Loading…
Add table
Add a link
Reference in a new issue