Skip to content

Commit

Permalink
feat: code sign builds (#13)
Browse files Browse the repository at this point in the history
* feat: code sign builds

* chore: upgrade setup-msbuild actions

* feat: make code signing optional, but default to true
  • Loading branch information
xeekworx authored Feb 3, 2024
1 parent 08d7d19 commit 5b7b68b
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 25 deletions.
90 changes: 66 additions & 24 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,43 +1,66 @@
---
name: Build

on:
workflow_dispatch:
inputs:
platform:
description: 'Build Platform'
description: "Build Platform"
required: true
type: choice
default: 'x64'
default: "x64"
options:
- x64
- x86
configuration:
description: 'Build Configuration'
description: "Build Configuration"
required: true
type: choice
default: 'Release'
default: "Release"
options:
- Release
- Debug
assembly:
description: "The built assembly name without the .exe extension"
required: false
type: string
default: "WindowTool"
enable_signing:
description: "Enable code signing"
required: false
type: boolean
default: true
workflow_call:
inputs:
platform:
description: 'Build Platform'
description: "Build Platform"
required: true
type: string
default: 'x64'
default: "x64"
configuration:
description: 'Build Configuration'
description: "Build Configuration"
required: true
type: string
default: 'Release'
default: "Release"
assembly:
description: "The built assembly name without the .exe extension"
required: false
type: string
default: "WindowTool"
enable_signing:
description: "Enable code signing"
required: false
type: boolean
default: true
pull_request:

env:
BUILD_CONFIG: ${{ inputs.configuration || 'Release' }}
PLATFORM: ${{ inputs.platform || 'x64' }}
SOLUTION_FILE: ./WindowTool.sln
PROJECT_FILE: ./WindowTool.vcxproj
OUTPUT_ASSEMBLY_NAME: ${{ inputs.assembly || 'WindowTool' }}
ENABLE_SIGNING: ${{ inputs.enable_signing || false }}
OUTPUT_DIR: ./bin/

jobs:
Expand All @@ -46,22 +69,41 @@ jobs:
runs-on: windows-latest

steps:
- name: Check Out Code
uses: actions/checkout@v4
- name: Check Out Code
uses: actions/checkout@v4

- name: Setup Microsoft Build Engine
uses: microsoft/setup-msbuild@v2

- name: Build
run: |
msbuild `
"/p:Configuration=${{ env.BUILD_CONFIG }}" `
"/p:Platform=${{ env.PLATFORM }}" `
"/p:OutDir=${{ env.OUTPUT_DIR }}${{ env.PLATFORM }}/" `
"/p:AssemblyName=${{ env.OUTPUT_ASSEMBLY_NAME }}" `
"${{ env.PROJECT_FILE }}"
- name: Setup Microsoft Build Engine
uses: microsoft/setup-msbuild@v1
- name: Decode and Save Code Signing Certificate
if: ${{ env.ENABLE_SIGNING == 'true' }}
run: |
$certBase64 = "${{ secrets.CODE_SIGN_CERT_BASE64 }}"
$certBytes = [System.Convert]::FromBase64String($certBase64)
$certPath = "code-sign-cert.pfx"
[System.IO.File]::WriteAllBytes($certPath, $certBytes)
shell: pwsh

- name: Build
run: |
msbuild `
"/p:Configuration=${{ env.BUILD_CONFIG }}" `
"/p:Platform=${{ env.PLATFORM }}" `
"/p:OutDir=${{ env.OUTPUT_DIR }}${{ env.PLATFORM }}/" `
"${{ env.PROJECT_FILE }}"
- name: Sign Executable
if: ${{ env.ENABLE_SIGNING == 'true' }}
run: |
$certPassword = "${{ secrets.CODE_SIGN_PASSWORD }}"
$secureCertPassword = ConvertTo-SecureString -String $certPassword -AsPlainText -Force
.\scripts\SignCode.ps1 -CertPath "code-sign-cert.pfx" -CertPassword $secureCertPassword -BinaryPath "${{ env.OUTPUT_DIR }}${{ env.PLATFORM }}/${{ env.OUTPUT_ASSEMBLY_NAME }}.exe"
shell: pwsh

- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ env.PLATFORM }}-artifact
path: ${{ env.OUTPUT_DIR }}${{ env.PLATFORM }}/
- name: Upload Artifacts
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v4
with:
name: ${{ env.PLATFORM }}-artifact
path: ${{ env.OUTPUT_DIR }}${{ env.PLATFORM }}/
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@v4

- name: Setup Microsoft Build Engine
uses: microsoft/setup-msbuild@v1
uses: microsoft/setup-msbuild@v2

- name: Restore NuGet Packages
working-directory: ${{ env.TEST_PROJECT_DIR }}
Expand Down
78 changes: 78 additions & 0 deletions scripts/GenerateCert.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<#
.SYNOPSIS
Generates a self-signed Code Signing certificate.
.DESCRIPTION
This script generates a self-signed Code Signing certificate for the specified company name,
exports it to a PFX file, and then removes the certificate from the certificate store.
.PARAMETER CompanyName
The name of the company for which the certificate is being generated. This name is used for the Common Name (CN) and Organization (O) fields in the certificate subject.
.PARAMETER ExportPath
The file path where the PFX file containing the self-signed certificate will be exported. The default value is "cert.pfx".
.PARAMETER CertPassword
The password to secure the exported PFX file. This parameter is mandatory and must be provided as a SecureString.
.PARAMETER YearsValid
The number of years the certificate will be valid for. The default value is 2 years.
.EXAMPLE
$SecurePassword = ConvertTo-SecureString -String "P@ssw0rd" -AsPlainText -Force
.\GenerateCert.ps1 -CompanyName "ExampleCompany" -ExportPath ".\ExampleCert.pfx" -CertPassword $SecurePassword
This example generates a certificate for "ExampleCompany", exports it to ".\ExampleCert.pfx", using the password "P@ssw0rd".
#>

param(
[Parameter(Mandatory=$true, HelpMessage="The name of the company for which the certificate is being generated. Used in the CN and O fields of the certificate subject.")]
[ValidateNotNullOrEmpty()]
[string]$CompanyName,

[Parameter(HelpMessage="The file path where the PFX file will be exported. Default is 'cert.pfx'.")]
[string]$ExportPath = "cert.pfx",

[Parameter(Mandatory=$true, HelpMessage="The password for the exported PFX file. Must be a SecureString.")]
[ValidateScript({
if (([Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($_))).Length -eq 0) {
throw "Certificate password must not be empty."
}
return $true
})]
[SecureString]$CertPassword,

[ValidateRange(1, 10)]
[int]$YearsValid = 2
)

# Check if ExportPath already exists and ask for overwrite confirmation
if (Test-Path $ExportPath) {
$overwrite = Read-Host "File '$ExportPath' already exists. Do you want to overwrite it? (Y/N)"
if ($overwrite -ne 'Y') {
Write-Host "Operation cancelled by user."
exit
}
}

$certSubject = "CN=$CompanyName CA, O=$CompanyName, C=US"
$certStoreLocation = "Cert:\CurrentUser\My"

# Generate a self-signed certificate
$cert = New-SelfSignedCertificate `
-Type CodeSigningCert -Subject $certSubject `
-TextExtension @("2.5.29.19={text}false") `
-KeyUsage DigitalSignature `
-KeyLength 2048 `
-NotAfter (Get-Date).AddYears($YearsValid) `
-CertStoreLocation $certStoreLocation `
-KeyExportPolicy Exportable

# Export the certificate to a PFX file
$certPath = "$certStoreLocation\" + $cert.Thumbprint
Export-PfxCertificate -Cert $certPath -FilePath $ExportPath -Password $CertPassword

# Remove the certificate from the store after export
Get-ChildItem -Path $certPath | Remove-Item

Write-Host "Certificate generated and exported to $ExportPath"
90 changes: 90 additions & 0 deletions scripts/SignCode.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<#
.SYNOPSIS
Signs a binary file with a digital certificate.
.DESCRIPTION
This script signs a specified binary file using a digital certificate. The certificate can be provided either as a file path or as a Base64-encoded string. A password for the certificate is required.
.PARAMETER CertPath
The file path to the digital certificate (.pfx file). If this parameter is not provided and CertBase64 is used, a temporary certificate file will be created.
.PARAMETER CertBase64
The Base64-encoded string of the digital certificate. If provided, it will be decoded and saved as a temporary .pfx file for signing. This parameter is ignored if CertPath is provided.
.PARAMETER CertPassword
The password for the digital certificate's private key. This parameter is mandatory and must be provided as a SecureString.
.PARAMETER BinaryPath
The file path to the binary that will be signed. This parameter is mandatory.
.EXAMPLE
PS> .\SignCode.ps1 -CertPath "C:\path\to\your\certificate.pfx" -CertPassword (ConvertTo-SecureString -String "YourPassword" -AsPlainText -Force) -BinaryPath "C:\path\to\your\binary.exe"
Signs the binary "binary.exe" using the certificate specified at "certificate.pfx".
.EXAMPLE
PS> $certBase64 = Get-Content "C:\path\to\your\certificateBase64.txt" -Raw
PS> .\SignCode.ps1 -CertBase64 $certBase64 -CertPassword (ConvertTo-SecureString -String "YourPassword" -AsPlainText -Force) -BinaryPath "C:\path\to\your\binary.exe"
Signs the binary "binary.exe" using a Base64-encoded digital certificate read from a text file.
.NOTES
This script uses signtool.exe, which must be available in your system's PATH. The script assumes the use of SHA256 for signing and timestamps the signature using http://timestamp.digicert.com.
#>

param(
[Parameter(Mandatory=$false)]
[string]$CertPath,

[Parameter(Mandatory=$false)]
[string]$CertBase64,

[Parameter(Mandatory=$true)]
[System.Security.SecureString]$CertPassword,

[Parameter(Mandatory=$true)]
[string]$BinaryPath
)

function Find-SignTool {
$possiblePaths = @(
"${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64",
"${env:ProgramFiles(x86)}\Windows Kits\10\bin\10.0.*\x64",
"${env:ProgramFiles(x86)}\Microsoft SDKs\Windows\v7.1A\Bin",
"${env:ProgramFiles}\Microsoft SDKs\Windows\v8.1A\Bin\x64"
)

foreach ($path in $possiblePaths) {
$signtool = Get-ChildItem -Path $path -Filter signtool.exe -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
if ($null -ne $signtool) {
return $signtool.FullName
}
}

throw "signtool.exe not found. Please ensure the Windows SDK is installed."
}

$signtoolPath = Find-SignTool

if ($CertBase64 -and !$CertPath) {
$certContent = [System.Convert]::FromBase64String($CertBase64)
$CertPath = "tempCert.pfx"
[IO.File]::WriteAllBytes($CertPath, $certContent)
}

if (-not (Test-Path $CertPath)) {
throw "Certificate path is invalid or certificate is not provided."
}

if (-not (Test-Path $BinaryPath)) {
throw "Binary path is invalid."
}

$certPasswordPlainText = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($CertPassword))

& $signtoolPath sign /fd SHA256 /a /f $CertPath /p $certPasswordPlainText /tr http://timestamp.digicert.com /td SHA256 $BinaryPath

if ($LASTEXITCODE -ne 0) {
throw "Signing failed with exit code $LASTEXITCODE."
}

0 comments on commit 5b7b68b

Please sign in to comment.