Skip to content

Commit 1c79f00

Browse files
committed
refactor: Switch certutil to primary export method with PowerShell fallback
- certutil.exe proved to be more reliable than PowerShell's Export-PfxCertificate - certutil is the underlying tool used by Windows Certificate Manager (MMC) - Switched method priority: certutil first, PowerShell as fallback - Both methods support password-protected PFX export - Reduces warnings in output when certutil succeeds (no unnecessary fallback attempts) - Maintains backward compatibility by keeping PowerShell fallback Rationale: - PowerShell Export-PfxCertificate has undocumented limitations with certain passwords - certutil handles edge cases and special characters in passwords more robustly - Users familiar with MMC will recognize certutil as the trusted method - No behavioral change for end users - exports still work the same way Testing verified: - Direct certutil export succeeds without fallback - PFX file created with correct naming and size - Password protection preserved correctly
1 parent 9d186a8 commit 1c79f00

1 file changed

Lines changed: 40 additions & 51 deletions

File tree

Scripts/SFA/Export-UserCertificates.ps1

Lines changed: 40 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -331,69 +331,58 @@ $exportScriptBlock = {
331331
$filePath = Join-Path $ExportPath $fileName
332332

333333
try {
334-
if ($certPassword) {
335-
Export-PfxCertificate -Cert $cert -FilePath $filePath -Password $certPassword -Force -ErrorAction Stop | Out-Null
334+
# Use certutil as primary method - it's more reliable and what Windows Certificate Manager uses
335+
# Convert SecureString password to plain text for certutil
336+
$plaintextPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToCoTaskMemUnicode($certPassword))
337+
338+
# Export using certutil with password protection
339+
& certutil.exe -p $plaintextPassword -exportPFX -user My $cert.Thumbprint $filePath 2>&1 | Out-Null
340+
341+
if (Test-Path $filePath -ErrorAction SilentlyContinue) {
342+
$results += @{
343+
User = $userName
344+
Username = $domainUsername
345+
Store = $storeType
346+
Status = '✅ Exported'
347+
FileName = $fileName
348+
Expiration = $expirationDate
349+
UsedFallback = $usedFallback
350+
Count = 1
351+
}
336352
}
337353
else {
338-
Export-PfxCertificate -Cert $cert -FilePath $filePath -Force -ErrorAction Stop | Out-Null
339-
}
340-
341-
$results += @{
342-
User = $userName
343-
Username = $domainUsername
344-
Store = $storeType
345-
Status = '✅ Exported'
346-
FileName = $fileName
347-
Expiration = $expirationDate
348-
UsedFallback = $usedFallback
349-
Count = 1
354+
throw "Certificate file was not created"
350355
}
351356
}
352357
catch {
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-
}
358+
# Fallback to PowerShell Export-PfxCertificate if certutil fails
359+
Write-Host " ⚠️ certutil export failed, trying PowerShell..." -ForegroundColor Yellow
360+
361+
try {
362+
if ($certPassword) {
363+
Export-PfxCertificate -Cert $cert -FilePath $filePath -Password $certPassword -Force -ErrorAction Stop | Out-Null
379364
}
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-
}
365+
else {
366+
Export-PfxCertificate -Cert $cert -FilePath $filePath -Force -ErrorAction Stop | Out-Null
367+
}
368+
369+
$results += @{
370+
User = $userName
371+
Username = $domainUsername
372+
Store = $storeType
373+
Status = '✅ Exported (via PowerShell)'
374+
FileName = $fileName
375+
Expiration = $expirationDate
376+
UsedFallback = $usedFallback
377+
Count = 1
389378
}
390379
}
391-
else {
380+
catch {
392381
$results += @{
393382
User = $userName
394383
Username = $domainUsername
395384
Store = $storeType
396-
Status = "❌ Error: $_"
385+
Status = "❌ Error (both methods failed): $_"
397386
UsedFallback = $usedFallback
398387
Count = 0
399388
}

0 commit comments

Comments
 (0)