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:
@@ -0,0 +1,5 @@
|
|||||||
|
# Sensitive / local files
|
||||||
|
*.json
|
||||||
|
!metadata.template.json
|
||||||
|
TrustedSigningClient/
|
||||||
|
*.log
|
||||||
@@ -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)
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user