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
This commit is contained in:
huanld
2026-05-18 14:54:06 +07:00
commit d78ca9b6ac
4 changed files with 368 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
# Sensitive / local files
*.json
!metadata.template.json
TrustedSigningClient/
*.log
+283
View File
@@ -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)
+16
View File
@@ -0,0 +1,16 @@
{
"Endpoint": "https://eus.codesigning.azure.net/",
"CodeSigningAccountName": "<YOUR_ACCOUNT_NAME>",
"CertificateProfileName": "<YOUR_PROFILE_NAME>",
"ExcludeCredentials": [
"WorkloadIdentityCredential",
"ManagedIdentityCredential",
"SharedTokenCacheCredential",
"VisualStudioCredential",
"VisualStudioCodeCredential",
"AzureCliCredential",
"AzurePowerShellCredential",
"AzureDeveloperCliCredential",
"InteractiveBrowserCredential"
]
}
+64
View File
@@ -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
}