Skip to content

Commit 9d186a8

Browse files
committed
fix: Add certutil fallback for non-exportable certificate private keys
- Export-PfxCertificate fails with 'Cannot export non-exportable private key' error on some certificates - Windows certutil.exe can successfully export these certificates where PowerShell cmdlets fail - Added automatic fallback: when Export-PfxCertificate fails, script attempts export using certutil.exe - Certutil fallback preserves PFX password protection using -p parameter - Script reports status as 'Exported (via certutil)' when fallback method is used - Resolves issue where valid certificates with non-exportable flags could not be distributed Testing verified: - PowerShell method used first (preferred when available) - Certutil fallback triggers only on 'non-exportable' errors - Both methods preserve password protection - Certificate file successfully created and accessible
1 parent 2291d51 commit 9d186a8

1 file changed

Lines changed: 49 additions & 9 deletions

File tree

Scripts/SFA/Export-UserCertificates.ps1

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -332,10 +332,10 @@ $exportScriptBlock = {
332332

333333
try {
334334
if ($certPassword) {
335-
Export-PfxCertificate -Cert $cert -FilePath $filePath -Password $certPassword -Force | Out-Null
335+
Export-PfxCertificate -Cert $cert -FilePath $filePath -Password $certPassword -Force -ErrorAction Stop | Out-Null
336336
}
337337
else {
338-
Export-PfxCertificate -Cert $cert -FilePath $filePath -Force | Out-Null
338+
Export-PfxCertificate -Cert $cert -FilePath $filePath -Force -ErrorAction Stop | Out-Null
339339
}
340340

341341
$results += @{
@@ -350,13 +350,53 @@ $exportScriptBlock = {
350350
}
351351
}
352352
catch {
353-
$results += @{
354-
User = $userName
355-
Username = $domainUsername
356-
Store = $storeType
357-
Status = "❌ Error: $_"
358-
UsedFallback = $usedFallback
359-
Count = 0
353+
# If PowerShell export fails with non-exportable error, try certutil as fallback
354+
if ($_ -like "*non-exportable*") {
355+
Write-Host " ⚠️ PowerShell export failed, trying certutil.exe..." -ForegroundColor Yellow
356+
357+
try {
358+
# Convert SecureString password to plain text for certutil
359+
$plaintextPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToCoTaskMemUnicode($certPassword))
360+
361+
# Export using certutil with password protection
362+
& certutil.exe -p $plaintextPassword -exportPFX -user My $cert.Thumbprint $filePath | Out-Null
363+
364+
if (Test-Path $filePath -ErrorAction SilentlyContinue) {
365+
$results += @{
366+
User = $userName
367+
Username = $domainUsername
368+
Store = $storeType
369+
Status = '✅ Exported (via certutil)'
370+
FileName = $fileName
371+
Expiration = $expirationDate
372+
UsedFallback = $usedFallback
373+
Count = 1
374+
}
375+
}
376+
else {
377+
throw "Certutil export file not created"
378+
}
379+
}
380+
catch {
381+
$results += @{
382+
User = $userName
383+
Username = $domainUsername
384+
Store = $storeType
385+
Status = "❌ Error (both methods failed): $_"
386+
UsedFallback = $usedFallback
387+
Count = 0
388+
}
389+
}
390+
}
391+
else {
392+
$results += @{
393+
User = $userName
394+
Username = $domainUsername
395+
Store = $storeType
396+
Status = "❌ Error: $_"
397+
UsedFallback = $usedFallback
398+
Count = 0
399+
}
360400
}
361401
}
362402
}

0 commit comments

Comments
 (0)