diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6574c88..3df8c5c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,6 +21,20 @@ jobs: - name: Build, test, and test-sign package shell: powershell run: .\build.ps1 -Version "${{ github.ref_name }}" -TestSign + - name: Verify signing material cleanup + if: always() + shell: powershell + run: | + $certificates = Get-ChildItem ` + Cert:\CurrentUser\My, ` + Cert:\LocalMachine\Root, ` + Cert:\LocalMachine\TrustedPublisher ` + -ErrorAction SilentlyContinue | + Where-Object Subject -eq "CN=Roxy Kernel Tools Test Signing" + if ($certificates) { + $certificates | Format-List PSParentPath, Thumbprint, HasPrivateKey + throw "Test-signing certificate or private-key material remains on the runner." + } - uses: actions/upload-artifact@v4 with: name: kernel-tools-windows-x64-${{ github.sha }} diff --git a/README.md b/README.md index 9e3b7df..34d0842 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,18 @@ certificate, and `roxy-kernel-bridge.exe`. Roxy downloads a pinned release, verifies its SHA-256 digest, and installs it only after explicit user confirmation and a UAC prompt. +The release workflow creates a one-build, non-exportable private key, embeds a +SHA-256 signature in the driver, verifies it with SignTool's Authenticode +policy, matches the embedded signer to the generated certificate, and deletes +the private key before uploading artifacts. CI temporarily trusts the public +certificate in the runner's machine stores so SignTool must report success, then +removes that trust. A final workflow guard fails if the certificate or private +key remains on the runner. Non-elevated local builds accept only SignTool's +expected self-signed-root error; any other verification failure stops the build. +Only the public certificate is published. During installation, Roxy adds that +certificate to the target machine's Trusted Root and Trusted Publishers stores and removes +only trust entries that Roxy added when Kernel Tools is uninstalled. + These development releases require Windows test-signing mode and trusting the included public certificate. Normal Secure Boot production deployment requires Microsoft attestation or WHQL signing; a GitHub-built test certificate is not a diff --git a/build.ps1 b/build.ps1 index 1dfcdff..cb61966 100644 --- a/build.ps1 +++ b/build.ps1 @@ -50,22 +50,56 @@ if ($TestSign) { -KeyAlgorithm RSA ` -KeyLength 3072 ` -HashAlgorithm SHA256 ` - -KeyExportPolicy Exportable ` + -KeyExportPolicy NonExportable ` -NotAfter (Get-Date).AddYears(3) - $signTool = Find-WdkTool "signtool.exe" - & $signTool sign /v /fd SHA256 /s My /sha1 $certificate.Thumbprint (Join-Path $package "aibridge.sys") - if ($LASTEXITCODE -ne 0) { throw "Driver signing failed." } - Copy-Item -Force (Join-Path $package "aibridge.sys") $driver - Export-Certificate -Cert $certificate -FilePath (Join-Path $package "aibridge-test.cer") | Out-Null -} + $rootTrustedForVerification = $false + $publisherTrustedForVerification = $false + try { + $signTool = Find-WdkTool "signtool.exe" + & $signTool sign /v /fd SHA256 /s My /sha1 $certificate.Thumbprint (Join-Path $package "aibridge.sys") + if ($LASTEXITCODE -ne 0) { throw "Driver signing failed." } + Copy-Item -Force (Join-Path $package "aibridge.sys") $driver + $certificatePath = Join-Path $package "aibridge-test.cer" + Export-Certificate -Cert $certificate -FilePath $certificatePath | Out-Null + if ($env:GITHUB_ACTIONS -eq "true") { + Import-Certificate -FilePath $certificatePath -CertStoreLocation "Cert:\LocalMachine\Root" | Out-Null + $rootTrustedForVerification = $true + Import-Certificate -FilePath $certificatePath -CertStoreLocation "Cert:\LocalMachine\TrustedPublisher" | Out-Null + $publisherTrustedForVerification = $true + } -if ($TestSign) { - $signature = Get-AuthenticodeSignature (Join-Path $package "aibridge.sys") - if (-not $signature.SignerCertificate -or $signature.SignerCertificate.Thumbprint -ne $certificate.Thumbprint) { - throw "The packaged driver is not signed by the generated test certificate." + $verifyOutputLog = Join-Path $dist "signtool-verify.stdout.log" + $verifyErrorLog = Join-Path $dist "signtool-verify.stderr.log" + $verifyProcess = Start-Process ` + -FilePath $signTool ` + -ArgumentList @("verify", "/v", "/pa", (Join-Path $package "aibridge.sys")) ` + -RedirectStandardOutput $verifyOutputLog ` + -RedirectStandardError $verifyErrorLog ` + -Wait ` + -PassThru + $verifyOutput = (Get-Content -Raw $verifyOutputLog), (Get-Content -Raw $verifyErrorLog) -join "`n" + Remove-Item $verifyOutputLog, $verifyErrorLog -Force + if ($verifyProcess.ExitCode -ne 0) { + if (($rootTrustedForVerification -and $publisherTrustedForVerification) -or $verifyOutput -notmatch "terminated in a root\s+certificate which is not trusted") { + throw "Driver signature verification failed.`n$verifyOutput" + } + } + Write-Host $verifyOutput + + $signature = Get-AuthenticodeSignature (Join-Path $package "aibridge.sys") + if (-not $signature.SignerCertificate -or $signature.SignerCertificate.Thumbprint -ne $certificate.Thumbprint) { + throw "The packaged driver is not signed by the generated test certificate." + } + } finally { + if ($rootTrustedForVerification) { + Remove-Item "Cert:\LocalMachine\Root\$($certificate.Thumbprint)" -Force -ErrorAction SilentlyContinue + } + if ($publisherTrustedForVerification) { + Remove-Item "Cert:\LocalMachine\TrustedPublisher\$($certificate.Thumbprint)" -Force -ErrorAction SilentlyContinue + } + Remove-Item "Cert:\CurrentUser\My\$($certificate.Thumbprint)" -DeleteKey -Force -ErrorAction SilentlyContinue } - Remove-Item "Cert:\CurrentUser\My\$($certificate.Thumbprint)" -Force -ErrorAction SilentlyContinue } $sourceCommit = (& git -C $root rev-parse HEAD).Trim()