# sign-release.ps1 # Tạo self-signed code signing certificate và ký các file .exe release # Cần chạy với quyền Administrator param( [string]$Subject = "CN=Tailscale-Custom, O=SoftsBusiness, C=VN", [string]$PfxPath = "$PSScriptRoot\tailscale-custom-codesign.pfx", [string]$CertPassword = "TailscaleCustomSign@2026", [string[]]$FilesToSign = @( "C:\Program Files\Tailscale-Custom\tailscaled.exe", "C:\Program Files\Tailscale-Custom\tailscale.exe" ), [string]$TimestampUrl = "http://timestamp.digicert.com", [switch]$RenewCert ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" # Self-elevate nếu chưa chạy với quyền Administrator $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator ) if (-not $isAdmin) { Write-Host "[..] Cần quyền Administrator, đang tự nâng quyền..." -ForegroundColor Yellow $logFile = "$env:TEMP\sign-release-log.txt" Remove-Item $logFile -ErrorAction SilentlyContinue $argString = "-ExecutionPolicy Bypass -File `"$PSCommandPath`"" Start-Process powershell -Verb RunAs -ArgumentList $argString -Wait # Hiện log sau khi elevated process hoàn thành Start-Sleep -Milliseconds 500 if (Test-Path $logFile) { Get-Content $logFile } else { Write-Warning "UAC bị từ chối hoặc script lỗi — không có log" } exit } # Ghi transcript ngay khi chạy với quyền Admin $transcriptFile = "$env:TEMP\sign-release-log.txt" Start-Transcript -Path $transcriptFile -Force | Out-Null # ───────────────────────────────────────────────────────────── # 1. Tạo hoặc load certificate # ───────────────────────────────────────────────────────────── $securePass = ConvertTo-SecureString $CertPassword -AsPlainText -Force # Kiểm tra cert đã có trong store chưa $existingCert = Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.Subject -eq $Subject -and $_.HasPrivateKey } | Sort-Object NotAfter -Descending | Select-Object -First 1 if ($existingCert -and -not $RenewCert) { Write-Host "[OK] Dùng cert hiện có: $($existingCert.Thumbprint) (hết hạn $($existingCert.NotAfter.ToString('yyyy-MM-dd')))" -ForegroundColor Green $cert = $existingCert } else { Write-Host "[..] Tạo self-signed code signing certificate..." -ForegroundColor Yellow $cert = New-SelfSignedCertificate ` -Subject $Subject ` -Type CodeSigningCert ` -KeyUsage DigitalSignature ` -KeyAlgorithm RSA ` -KeyLength 4096 ` -HashAlgorithm SHA256 ` -NotAfter (Get-Date).AddYears(10) ` -CertStoreLocation "Cert:\LocalMachine\My" ` -FriendlyName "Tailscale-Custom Code Signing" Write-Host "[OK] Cert tạo xong: $($cert.Thumbprint)" -ForegroundColor Green Write-Host " Subject : $($cert.Subject)" Write-Host " Hết hạn : $($cert.NotAfter.ToString('yyyy-MM-dd'))" # Export PFX (chứa private key) Export-PfxCertificate -Cert $cert -FilePath $PfxPath -Password $securePass | Out-Null Write-Host "[OK] PFX đã lưu: $PfxPath" -ForegroundColor Green } # ───────────────────────────────────────────────────────────── # 2. Tin tưởng cert (Trusted Root CA + Trusted Publishers) # Cần thiết để Windows không báo "Unknown Publisher" # ───────────────────────────────────────────────────────────── function Add-CertToStore { param( [System.Security.Cryptography.X509Certificates.StoreName]$StoreName, [System.Security.Cryptography.X509Certificates.StoreLocation]$StoreLocation, [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate ) $store = New-Object System.Security.Cryptography.X509Certificates.X509Store($StoreName, $StoreLocation) $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) $existing = $store.Certificates | Where-Object { $_.Thumbprint -eq $Certificate.Thumbprint } if (-not $existing) { $store.Add($Certificate) Write-Host "[OK] Đã thêm cert vào store: $StoreName" -ForegroundColor Green } else { Write-Host "[--] Cert đã có trong store: $StoreName" -ForegroundColor DarkGray } $store.Close() } Add-CertToStore -StoreName Root -StoreLocation LocalMachine -Certificate $cert Add-CertToStore -StoreName TrustedPublisher -StoreLocation LocalMachine -Certificate $cert # ───────────────────────────────────────────────────────────── # 3. Ký từng file bằng Set-AuthenticodeSignature (built-in) # ───────────────────────────────────────────────────────────── Write-Host "`n[..] Bắt đầu ký file..." -ForegroundColor Yellow $signed = 0 $skipped = 0 $failed = 0 foreach ($file in $FilesToSign) { if (-not (Test-Path $file)) { Write-Warning "Không tìm thấy file: $file (bỏ qua)" $skipped++ continue } Write-Host " Đang ký: $file" try { # Thử ký với timestamp server trước $result = Set-AuthenticodeSignature ` -FilePath $file ` -Certificate $cert ` -TimestampServer $TimestampUrl ` -HashAlgorithm SHA256 ` -ErrorAction Stop if ($result.Status -eq "Valid") { Write-Host " [OK] Ký thành công: $(Split-Path $file -Leaf)" -ForegroundColor Green $signed++ } else { # Fallback: ký không có timestamp Write-Warning " Timestamp lỗi ($($result.StatusMessage)), thử lại không có timestamp..." $result = Set-AuthenticodeSignature ` -FilePath $file ` -Certificate $cert ` -HashAlgorithm SHA256 ` -ErrorAction Stop if ($result.Status -eq "Valid") { Write-Host " [OK] Ký thành công (no timestamp): $(Split-Path $file -Leaf)" -ForegroundColor Yellow $signed++ } else { Write-Host " [FAIL] Ký thất bại: $(Split-Path $file -Leaf) — $($result.StatusMessage)" -ForegroundColor Red $failed++ } } } catch { Write-Host " [FAIL] Exception: $_" -ForegroundColor Red $failed++ } } # ───────────────────────────────────────────────────────────── # 4. Xác nhận chữ ký # ───────────────────────────────────────────────────────────── if ($signed -gt 0) { Write-Host "`n[..] Xác nhận chữ ký..." -ForegroundColor Yellow foreach ($file in $FilesToSign) { if (-not (Test-Path $file)) { continue } $sig = Get-AuthenticodeSignature -FilePath $file $leaf = Split-Path $file -Leaf if ($sig.Status -eq "Valid") { Write-Host " [OK] Hợp lệ : $leaf" -ForegroundColor Green } elseif ($sig.Status -in @("UnknownError", "HashMismatch", "NotSigned")) { Write-Host " [~] Signed (self-signed): $leaf — $($sig.StatusMessage)" -ForegroundColor Yellow } else { Write-Warning " Trạng thái: $($sig.Status) — $leaf" } } } # ───────────────────────────────────────────────────────────── # 5. Tóm tắt # ───────────────────────────────────────────────────────────── Write-Host "`n════════════════════════════════════════" -ForegroundColor Cyan Write-Host " KẾT QUẢ KÝ FILE" -ForegroundColor Cyan Write-Host "════════════════════════════════════════" -ForegroundColor Cyan Write-Host " Ký thành công : $signed" Write-Host " Bỏ qua : $skipped (file không tồn tại)" Write-Host " Thất bại : $failed" Write-Host " Thumbprint : $($cert.Thumbprint)" Write-Host " PFX file : $PfxPath" Write-Host "════════════════════════════════════════" -ForegroundColor Cyan Stop-Transcript | Out-Null if ($failed -gt 0) { exit 1 }