Dargslan.RdpSign.psm1
|
<# .SYNOPSIS Bulk-generate and digitally sign Remote Desktop Protocol (.rdp) files. .DESCRIPTION Part of the Dargslan Windows Admin Tools collection. Wraps rdpsign.exe with safe defaults so you can mass-produce per-user signed .rdp files from a CSV + template in one call. More tools and resources at https://dargslan.com Free Cheat Sheet: https://dargslan.com/cheat-sheets/rdpsign-bulk-signing-2026 Full Guide: https://dargslan.com/blog/bulk-rdp-file-signing-rdpsign-powershell-2026 Linux & DevOps Books: https://dargslan.com/books .LINK https://dargslan.com .LINK https://github.com/Dargslan/powershell-admin-scripts #> function Write-DargslanBanner { $banner = @" +----------------------------------------------------------+ | Dargslan RdpSign - Bulk RDP File Signing Toolkit | | More tools: https://dargslan.com | | Cheat Sheets: https://dargslan.com/cheat-sheets | +----------------------------------------------------------+ "@ Write-Host $banner -ForegroundColor Cyan } function Get-DargslanCodeSigningCert { <# .SYNOPSIS List all code-signing certificates available in the current user and local machine certificate stores. .EXAMPLE Get-DargslanCodeSigningCert #> [CmdletBinding()] param( [switch]$IncludeExpired ) Write-DargslanBanner # Collect from both stores; LocalMachine may need admin to enumerate $stores = @('Cert:\CurrentUser\My','Cert:\LocalMachine\My') $results = @() foreach ($store in $stores) { try { $certs = Get-ChildItem -Path $store -CodeSigningCert -ErrorAction SilentlyContinue foreach ($c in $certs) { if (-not $IncludeExpired -and $c.NotAfter -lt (Get-Date)) { continue } $results += [PSCustomObject]@{ Store = $store Subject = $c.Subject Thumbprint = $c.Thumbprint NotAfter = $c.NotAfter Expired = ($c.NotAfter -lt (Get-Date)) } } } catch { # Likely permission issue on LocalMachine without admin - skip silently } } if (-not $results) { Write-Warning "No code-signing certificates found. Issue one from your AD CS code-signing template, or import a public CA cert." return } $results | Sort-Object Store, NotAfter } function Test-DargslanRdpSignature { <# .SYNOPSIS Dry-run signature test using rdpsign.exe /l. Does not modify the file. .EXAMPLE Test-DargslanRdpSignature -Thumbprint ABC... -Path C:\RDP\out\jsmith.rdp #> [CmdletBinding()] param( [Parameter(Mandatory=$true)][string]$Thumbprint, [Parameter(Mandatory=$true)][string]$Path ) # Strip any spaces a user might have copied with the thumbprint $Thumbprint = $Thumbprint -replace '\s','' if (-not (Test-Path -LiteralPath $Path)) { Write-Error "RDP file not found: $Path" return } $rdpsign = (Get-Command rdpsign.exe -ErrorAction SilentlyContinue) if (-not $rdpsign) { Write-Error "rdpsign.exe not found on PATH. It ships with Windows." return } $full = (Resolve-Path -LiteralPath $Path).Path & rdpsign.exe /sha256 $Thumbprint /l $full | Out-Null $exit = $LASTEXITCODE [PSCustomObject]@{ Path = $full Thumbprint = $Thumbprint ExitCode = $exit Success = ($exit -eq 0) } } function _Expand-Template { # Internal helper - expand __KEY__ placeholders from a hashtable param( [string]$Template, [hashtable]$Values ) $out = $Template foreach ($k in $Values.Keys) { $token = "__${k}__" $out = $out.Replace($token, [string]$Values[$k]) } return $out } function New-DargslanSignedRdp { <# .SYNOPSIS Generate a single .rdp file from a template, replace placeholders, and digitally sign it with rdpsign.exe. .EXAMPLE New-DargslanSignedRdp ` -TemplatePath C:\RDP\template.rdp ` -OutFile C:\RDP\out\jsmith.rdp ` -Values @{ USERNAME = 'DOMAIN\jsmith' } ` -Thumbprint ABCDEF1234567890ABCDEF1234567890ABCDEF12 #> [CmdletBinding()] param( [Parameter(Mandatory=$true)][string]$TemplatePath, [Parameter(Mandatory=$true)][string]$OutFile, [Parameter(Mandatory=$true)][hashtable]$Values, [Parameter(Mandatory=$true)][string]$Thumbprint, [switch]$WhatIfSign # if set, only validates the cert (rdpsign /l) and does not sign ) $Thumbprint = $Thumbprint -replace '\s','' if (-not (Test-Path -LiteralPath $TemplatePath)) { Write-Error "Template not found: $TemplatePath"; return } # Expand placeholders $template = Get-Content -LiteralPath $TemplatePath -Raw $content = _Expand-Template -Template $template -Values $Values # Ensure output directory exists $outDir = Split-Path -Parent $OutFile if ($outDir -and -not (Test-Path -LiteralPath $outDir)) { New-Item -ItemType Directory -Force -Path $outDir | Out-Null } # rdpsign expects ASCII-encoded .rdp Set-Content -LiteralPath $OutFile -Value $content -Encoding ASCII $full = (Resolve-Path -LiteralPath $OutFile).Path if ($WhatIfSign) { & rdpsign.exe /sha256 $Thumbprint /l $full | Out-Null } else { & rdpsign.exe /sha256 $Thumbprint $full | Out-Null } $exit = $LASTEXITCODE [PSCustomObject]@{ OutFile = $full ExitCode = $exit Signed = (-not $WhatIfSign -and $exit -eq 0) Tested = ($WhatIfSign -and $exit -eq 0) } } function New-DargslanSignedRdpBatch { <# .SYNOPSIS Bulk-generate + sign one .rdp per row of a CSV using a template. .DESCRIPTION The CSV must contain at least an OutputName column. Every other column is treated as a placeholder: column "UserName" expands the token __USERNAME__, "Gateway" expands __GATEWAY__, and so on (column names are upper-cased to form the token). .EXAMPLE New-DargslanSignedRdpBatch ` -TemplatePath C:\RDP\template.rdp ` -CsvPath C:\RDP\users.csv ` -OutDir C:\RDP\out ` -Thumbprint ABCDEF1234567890ABCDEF1234567890ABCDEF12 #> [CmdletBinding()] param( [Parameter(Mandatory=$true)][string]$TemplatePath, [Parameter(Mandatory=$true)][string]$CsvPath, [Parameter(Mandatory=$true)][string]$OutDir, [Parameter(Mandatory=$true)][string]$Thumbprint, [string]$OutputColumn = 'OutputName', [switch]$BatchSign # build all files first, then sign in one rdpsign call (faster for large lists) ) Write-DargslanBanner $Thumbprint = $Thumbprint -replace '\s','' if (-not (Test-Path -LiteralPath $TemplatePath)) { Write-Error "Template not found: $TemplatePath"; return } if (-not (Test-Path -LiteralPath $CsvPath)) { Write-Error "CSV not found: $CsvPath"; return } if (-not (Test-Path -LiteralPath $OutDir)) { New-Item -ItemType Directory -Force -Path $OutDir | Out-Null } $template = Get-Content -LiteralPath $TemplatePath -Raw $rows = Import-Csv -LiteralPath $CsvPath if (-not $rows) { Write-Warning "CSV is empty: $CsvPath"; return } $generatedFiles = @() $results = @() foreach ($row in $rows) { $outName = $row.$OutputColumn if (-not $outName) { Write-Warning "Row missing $OutputColumn column - skipping" continue } # Build the placeholder map from every property except OutputName $values = @{} foreach ($prop in $row.PSObject.Properties) { if ($prop.Name -eq $OutputColumn) { continue } $values[$prop.Name.ToUpperInvariant()] = $prop.Value } $content = _Expand-Template -Template $template -Values $values $outFile = Join-Path $OutDir $outName Set-Content -LiteralPath $outFile -Value $content -Encoding ASCII $generatedFiles += (Resolve-Path -LiteralPath $outFile).Path if (-not $BatchSign) { & rdpsign.exe /sha256 $Thumbprint (Resolve-Path -LiteralPath $outFile).Path | Out-Null $exit = $LASTEXITCODE $results += [PSCustomObject]@{ OutFile = $outFile Signed = ($exit -eq 0) ExitCode = $exit } if ($exit -eq 0) { Write-Host ("OK {0}" -f $outFile) -ForegroundColor Green } else { Write-Host ("FAIL {0} (exit {1})" -f $outFile, $exit) -ForegroundColor Red } } } if ($BatchSign -and $generatedFiles.Count -gt 0) { # One rdpsign call for all files - fastest path for hundreds of users & rdpsign.exe /sha256 $Thumbprint @generatedFiles | Out-Null $exit = $LASTEXITCODE foreach ($f in $generatedFiles) { $results += [PSCustomObject]@{ OutFile = $f Signed = ($exit -eq 0) ExitCode = $exit } } if ($exit -eq 0) { Write-Host ("Batch sign OK - {0} files signed" -f $generatedFiles.Count) -ForegroundColor Green } else { Write-Host ("Batch sign FAILED (exit {0})" -f $exit) -ForegroundColor Red } } $results } Export-ModuleMember -Function Get-DargslanCodeSigningCert, Test-DargslanRdpSignature, New-DargslanSignedRdp, New-DargslanSignedRdpBatch |