commit d78ca9b6ac9f9aeaa2734f5fc9645a6a36dd9832 Author: huanld Date: Mon May 18 14:54:06 2026 +0700 feat: initial commit - Azure Trusted Signing automation script - sign-app.ps1: PowerShell signing script using signtool.exe + Trusted Signing dlib - README.md: comprehensive setup and usage documentation - metadata.template.json: signing metadata template (no secrets) - Service Principal auth: AZURE_CLIENT_ID/SECRET/TENANT_ID via EnvironmentCredential diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ee37064 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Sensitive / local files +*.json +!metadata.template.json +TrustedSigningClient/ +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..d628821 --- /dev/null +++ b/README.md @@ -0,0 +1,283 @@ +# 🔐 Azure Trusted Signing Tool + +> Script tự động ký số (code signing) Windows executables bằng **Azure Trusted Signing** — không cần Azure CLI, không cần browser login. + +## Mục lục + +- [Tổng quan](#tổng-quan) +- [Yêu cầu](#yêu-cầu) +- [Cấu hình Azure](#cấu-hình-azure) +- [Cài đặt](#cài-đặt) +- [Sử dụng](#sử-dụng) +- [Tích hợp CI/CD](#tích-hợp-cicd) +- [Troubleshooting](#troubleshooting) + +--- + +## Tổng quan + +Tool này sử dụng: +- **`signtool.exe`** (Windows SDK) — công cụ ký số chuẩn của Microsoft +- **`Microsoft.Trusted.Signing.Client`** dlib — thư viện Azure Trusted Signing +- **Service Principal** (App Registration) — xác thực tự động, không cần tương tác + +### Kiến trúc + +``` +sign-app.ps1 + │ + ├── Đọc AZURE_CLIENT_ID / SECRET / TENANT_ID + ├── Tạo metadata.json (BOM-free) + │ + └── signtool.exe sign /dlib Azure.CodeSigning.Dlib.dll /dmdf metadata.json + │ + └── Azure Trusted Signing API (eus.codesigning.azure.net) + └── Ký hash → trả về signature → nhúng vào .exe +``` + +--- + +## Yêu cầu + +| Thành phần | Version | Ghi chú | +|-----------|---------|---------| +| Windows | 10/11 hoặc Server 2019+ | Bắt buộc | +| PowerShell | 5.1+ | Có sẵn trên Windows | +| Windows SDK | 10.0.26100+ | Chứa `signtool.exe` | +| .NET SDK | 8.0+ | Để build project | + +--- + +## Cấu hình Azure + +### Tài nguyên đã cấu hình + +| Thông số | Giá trị | +|---------|---------| +| Tenant ID | `c4f27370-f4da-4e92-b944-6adc120b3683` | +| Trusted Signing Endpoint | `https://eus.codesigning.azure.net/` | +| Account Name | `huanld` | +| Certificate Profile | `codesign-profile` | +| Trust Type | Private Trust | +| App Registration | `trusted-signing-sp` | +| Client ID | `0a2888b4-baa5-48da-abea-24f2e9a1f92f` | +| Secret Expires | 2028-05-17 | + +### Role Assignment + +Service Principal `trusted-signing-sp` có role: +- **Trusted Signing Certificate Profile Signer** trên account `huanld` + +### Setup Azure (lần đầu tiên) + +1. **Tạo Trusted Signing Account** tại [Azure Portal](https://portal.azure.com): + - Resource Group: `signcode` + - Account Name: `huanld` + - Region: `East US` + +2. **Tạo Certificate Profile**: + - Profile Name: `codesign-profile` + - Profile Type: `Private Trust` + +3. **Tạo App Registration**: + ``` + Azure Portal → App registrations → New registration + Name: trusted-signing-sp + ``` + +4. **Tạo Client Secret**: + ``` + App registration → Certificates & secrets → New client secret + Expiry: 24 months + ``` + +5. **Gán Role**: + ``` + Trusted Signing Account → Access control (IAM) → Add role assignment + Role: Trusted Signing Certificate Profile Signer + Member: trusted-signing-sp + ``` + +--- + +## Cài đặt + +### 1. Tải Microsoft.Trusted.Signing.Client + +Script tự động tải nếu chưa có. Hoặc tải thủ công: + +```powershell +$dir = "$env:TEMP\TrustedSigningClient" +New-Item -ItemType Directory -Force -Path $dir | Out-Null +Invoke-WebRequest "https://www.nuget.org/api/v2/package/Microsoft.Trusted.Signing.Client" ` + -OutFile "$dir\TrustedSigningClient.zip" -UseBasicParsing +Expand-Archive "$dir\TrustedSigningClient.zip" -DestinationPath $dir -Force +``` + +### 2. Xác nhận signtool.exe + +```powershell +Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\bin\*\x86\signtool.exe" | + Sort-Object FullName -Descending | Select-Object -First 1 +``` + +Kết quả mong đợi: +``` +C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x86\signtool.exe +``` + +--- + +## Sử dụng + +### Cú pháp + +```powershell +powershell -ExecutionPolicy Bypass -File sign-app.ps1 -FilePath <đường_dẫn_file> +``` + +### Ví dụ + +```powershell +# Ký file exe đơn +powershell -ExecutionPolicy Bypass -File sign-app.ps1 -FilePath "C:\build\MyApp.exe" + +# Ký file trong thư mục publish +powershell -ExecutionPolicy Bypass -File sign-app.ps1 ` + -FilePath "D:\Code\MyProject\bin\Release\net8.0\publish\MyApp.exe" + +# Ký nhiều file (loop) +Get-ChildItem "D:\build\publish" -Filter "*.exe" | ForEach-Object { + powershell -ExecutionPolicy Bypass -File sign-app.ps1 -FilePath $_.FullName +} +``` + +### Tham số tùy chỉnh + +```powershell +powershell -ExecutionPolicy Bypass -File sign-app.ps1 ` + -FilePath "C:\build\MyApp.exe" ` + -AccountUrl "https://eus.codesigning.azure.net/" ` + -AccountName "huanld" ` + -ProfileName "codesign-profile" ` + -TimestampUrl "http://timestamp.acs.microsoft.com" ` + -SigntoolPath "C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x86\signtool.exe" +``` + +### Output + +``` +[INFO] Signing: C:\build\MyApp.exe + Account : huanld / codesign-profile + +Trusted Signing +Version: 1.0.95 +Submitting digest for signing... +OperationId abc123: InProgress +Signing completed with status 'Succeeded' in 2.3s +Successfully signed: C:\build\MyApp.exe + +[OK] Signed successfully! + Status : Valid + Signer : CN=huanleducoutlook.onmicrosoft.com, ... + Expires : 2026-06-18 +``` + +### Xác nhận chữ ký + +```powershell +$sig = Get-AuthenticodeSignature "C:\build\MyApp.exe" +$sig | Format-List Status, SignerCertificate +``` + +--- + +## Tích hợp CI/CD + +### PowerShell Build Script + +```powershell +# build-and-sign.ps1 +param([string]$Configuration = "Release") + +$projectPath = "D:\Code\MyProject" +$signScript = "$PSScriptRoot\sign-app.ps1" + +# Build +dotnet publish $projectPath -c $Configuration -o "$projectPath\publish" + +# Sign all executables +Get-ChildItem "$projectPath\publish" -Filter "*.exe" | ForEach-Object { + Write-Host "Signing: $($_.Name)" + & powershell -ExecutionPolicy Bypass -File $signScript -FilePath $_.FullName + if ($LASTEXITCODE -ne 0) { throw "Signing failed for $($_.Name)" } +} + +Write-Host "All files signed successfully!" +``` + +### GitHub Actions / Gitea Actions + +```yaml +- name: Sign Executables + shell: pwsh + env: + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + run: | + powershell -ExecutionPolicy Bypass -File sign-app.ps1 ` + -FilePath "${{ github.workspace }}\publish\MyApp.exe" +``` + +> ⚠️ Trong CI/CD, đặt credentials vào **Secrets** thay vì hardcode trong script. + +--- + +## Troubleshooting + +### ❌ `SignerSign() failed` / BOM error + +**Nguyên nhân**: metadata.json có BOM (Byte Order Mark). +**Giải pháp**: Script đã xử lý tự động bằng `UTF8Encoding(false)`. + +### ❌ `Selected user account does not exist in tenant 'Microsoft Services'` + +**Nguyên nhân**: Dùng sai client ID (Azure CLI app `04b07795...`). +**Giải pháp**: Dùng App Registration riêng trong tenant của bạn. + +### ❌ `The request body must contain 'code'` (AADSTS900144) + +**Nguyên nhân**: Dùng OAuth2 v1 endpoint với sai tên parameter. +**Giải pháp**: Dùng v2.0 endpoint + hashtable body (đã sửa trong script hiện tại). + +### ❌ `Signing failed` với status `UnknownError` + +**Nguyên nhân**: Certificate Profile là **Private Trust** — chỉ verify được trong môi trường tin tưởng nội bộ. +**Giải pháp**: Upgrade lên **Public Trust** để distribute ra ngoài (yêu cầu địa chỉ tổ chức tại Mỹ/EU và Microsoft vetting). + +### ❌ `signtool.exe not found` + +```powershell +# Tìm signtool.exe +Get-ChildItem "C:\Program Files (x86)\Windows Kits" -Filter "signtool.exe" -Recurse | + Sort-Object FullName -Descending | Select-Object -First 1 FullName +``` + +Nếu không có, cài Windows SDK từ: +``` +https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/ +``` + +### ❌ Role assignment chưa có hiệu lực + +Role mới gán cần **5-10 phút** để propagate. Chờ rồi thử lại. + +--- + +## Tham khảo + +- [Azure Trusted Signing Docs](https://learn.microsoft.com/en-us/azure/trusted-signing/) +- [Microsoft.Trusted.Signing.Client NuGet](https://www.nuget.org/packages/Microsoft.Trusted.Signing.Client) +- [signtool.exe Reference](https://learn.microsoft.com/en-us/windows/win32/seccrypto/signtool) +- [Azure Identity - DefaultAzureCredential](https://learn.microsoft.com/en-us/dotnet/api/azure.identity.defaultazurecredential) diff --git a/metadata.template.json b/metadata.template.json new file mode 100644 index 0000000..3c83443 --- /dev/null +++ b/metadata.template.json @@ -0,0 +1,16 @@ +{ + "Endpoint": "https://eus.codesigning.azure.net/", + "CodeSigningAccountName": "", + "CertificateProfileName": "", + "ExcludeCredentials": [ + "WorkloadIdentityCredential", + "ManagedIdentityCredential", + "SharedTokenCacheCredential", + "VisualStudioCredential", + "VisualStudioCodeCredential", + "AzureCliCredential", + "AzurePowerShellCredential", + "AzureDeveloperCliCredential", + "InteractiveBrowserCredential" + ] +} diff --git a/sign-app.ps1 b/sign-app.ps1 new file mode 100644 index 0000000..788c17a --- /dev/null +++ b/sign-app.ps1 @@ -0,0 +1,64 @@ +param( + [Parameter(Mandatory = $true)] + [string]$FilePath, + + [string]$AccountUrl = "https://eus.codesigning.azure.net/", + [string]$AccountName = "huanld", + [string]$ProfileName = "codesign-profile", + [string]$TimestampUrl = "http://timestamp.acs.microsoft.com", + [string]$SigntoolPath = "C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x86\signtool.exe", + [string]$DlibPath = "$env:TEMP\TrustedSigningClient\bin\x86\Azure.CodeSigning.Dlib.dll" +) + +$ErrorActionPreference = "Stop" + +# ── Service Principal Credentials ────────────────────────────────────────── +$env:AZURE_CLIENT_ID = "0a2888b4-baa5-48da-abea-24f2e9a1f92f" +$env:AZURE_CLIENT_SECRET = "QBe8Q~AdrbDO3XqWG.VFHbepXe3Pf4jJYIdKbdkA" +$env:AZURE_TENANT_ID = "c4f27370-f4da-4e92-b944-6adc120b3683" + +# ── Validate tools ────────────────────────────────────────────────────────── +if (-not (Test-Path $FilePath)) { Write-Error "File not found: $FilePath"; exit 1 } +if (-not (Test-Path $SigntoolPath)){ Write-Error "signtool.exe not found: $SigntoolPath"; exit 1 } +if (-not (Test-Path $DlibPath)) { + Write-Host "[INFO] Downloading Microsoft.Trusted.Signing.Client..." + $pkgDir = "$env:TEMP\TrustedSigningClient" + New-Item -ItemType Directory -Force -Path $pkgDir | Out-Null + $zip = "$pkgDir\TrustedSigningClient.zip" + Invoke-WebRequest "https://www.nuget.org/api/v2/package/Microsoft.Trusted.Signing.Client" ` + -OutFile $zip -UseBasicParsing + Expand-Archive -Path $zip -DestinationPath $pkgDir -Force + if (-not (Test-Path $DlibPath)) { Write-Error "Dlib not found after download."; exit 1 } + Write-Host "[INFO] Dlib ready." +} + +# ── Create metadata.json (no BOM) ────────────────────────────────────────── +$metadataPath = "$env:TEMP\TrustedSigningClient\metadata.json" +$metadataJson = @" +{"Endpoint":"$AccountUrl","CodeSigningAccountName":"$AccountName","CertificateProfileName":"$ProfileName","ExcludeCredentials":["WorkloadIdentityCredential","ManagedIdentityCredential","SharedTokenCacheCredential","VisualStudioCredential","VisualStudioCodeCredential","AzureCliCredential","AzurePowerShellCredential","AzureDeveloperCliCredential","InteractiveBrowserCredential"]} +"@ +[System.IO.File]::WriteAllText($metadataPath, $metadataJson.Trim(), [System.Text.UTF8Encoding]::new($false)) + +# ── Sign ──────────────────────────────────────────────────────────────────── +Write-Host "" +Write-Host "[INFO] Signing: $FilePath" +Write-Host " Account : $AccountName / $ProfileName" +Write-Host "" + +& $SigntoolPath sign /v /fd sha256 ` + /tr $TimestampUrl /td sha256 ` + /dlib $DlibPath ` + /dmdf $metadataPath ` + $FilePath + +if ($LASTEXITCODE -eq 0) { + $sig = Get-AuthenticodeSignature $FilePath + Write-Host "" + Write-Host "[OK] Signed successfully!" + Write-Host " Status : $($sig.Status)" + Write-Host " Signer : $($sig.SignerCertificate.Subject)" + Write-Host " Expires : $($sig.SignerCertificate.NotAfter.ToString('yyyy-MM-dd'))" +} else { + Write-Error "Signing failed! Exit code: $LASTEXITCODE" + exit $LASTEXITCODE +} \ No newline at end of file