question

PrinsAndre-6100 avatar image
0 Votes"
PrinsAndre-6100 asked PrinsAndre-6100 edited

export certificates using powershell Export-PfxCertificate : Cannot export non-exportable private key

in my previous question I showed the script to create certificates - and after fixing the file with the server names, it works fine.
Now I also want to automatically export the certificates.
But I am running into the error Export-PfxCertificate : Cannot export non-exportable private key

does anyone have an idea WHY I get this error (it is exportable) and how to fix it ???

ODD: I can manually export the certificate (that fails with the above error) fine (in MMC Certificates).
I am running powershell with my adminaccount - disabled UAC - it finds the certificates.... but the export fails -and I run MMC also with that same account

ODD too: when I am creating the certificates manually via the website, and then use my script running with my adminaccount, it will successfully export the certificate !!
Soo... I think it has something to do with how I create my certificate. I compared one that i created manually against a certificate created by the script, and I did not find any differences that might explain it. the only difference is, that when I create certificates via the website, the certificates are imported in Current User\Personal.
using the script, they are added to Local Computer\Personal - but manual export works, so I don't think that is the cause.


78636-image.png
this is how they show under Local Computer - Personal

when I create the certificates, I am using these lines:

 [NewRequest]
 Subject = "E=$E,CN=$CN,C=$c, S=$s, L=$l, O=$o, OU=$OU"
 MachineKeySet = TRUE
 UseExistingKeySet = False
 KeyLength = 2048
 KeySpec=1
 Exportable = TRUE
 RequestType = PKCS10
 ProviderName = "Microsoft Enhanced Cryptographic Provider v1.0" 
 FriendlyName = "$FriendlyName"
 [RequestAttributes]
 CertificateTemplate = "$TemplateName


I have seen some articles about this, but I have not been able to figure out the solution...


here is the export script, I have tried in 2 ways to export the certificate
like this: Export-PfxCertificate -Cert $cert -FilePath $outname -Password $mypwd -ChainOption EndEntityCertOnly -NoProperties -Verbose
and then commented it out, and added the other method below - but both give the same result

 $outpath = "C:\Certificates"
 $servers = get-content "C:\Certificates\servers.txt"
 $server1 = @()
 #create an array with the servers which I want to export
 foreach ($S in $servers)
 {
     if ($S.IndexOf(".") -gt 1)
     {$server1 += $S.Substring(0,$S.IndexOf("."))}
     else
     {$server1 += $S}
 }
    
 $mypwd = ConvertTo-SecureString -String "P@ssw0rd" -Force -AsPlainText
 $certs = Get-ChildItem -Path cert:\LocalMachine\my    #currentuser  LocalMachine
 foreach ($cert in $certs)
 {
     if ($cert.Issuer -eq "CN=Managed CA, DC=Domain, DC=com" -and $cert.FriendlyName -in $server1)
     {
         $name = $cert.DnsNameList.UniCode
         $CertFileName = $cert.FriendlyName
         $outname = $outpath + "\" + $CertFileName + ".pfx"
         #Export-PfxCertificate -Cert $cert -FilePath $outname -Password $mypwd -ChainOption EndEntityCertOnly -NoProperties -Verbose
         Get-ChildItem -Path ("Cert:\LocalMachine\my\" + $cert.Thumbprint)|Export-PfxCertificate -FilePath $outname -Password $mypwd -ChainOption EndEntityCertOnly -NoProperties
         $contd = read-host ("do you want to delete: " + $cert.FriendlyName + " - " + $cert.Subject)
         if ($contd -match 'y')
         {
             $path = "Cert:\LocalMachine\my\" + $cert.Thumbprint
             Get-ChildItem $path | Remove-Item -Force
         }
            
     }
 }

here the same certificate - but now manually exporting
78723-image.png





windows-server-powershell
image.png (1.4 KiB)
image.png (24.8 KiB)
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

PrinsAndre-6100 avatar image
0 Votes"
PrinsAndre-6100 answered PrinsAndre-6100 commented

OMG, I may just have found the root cause....
the newly created certificate - I checked the option "manage private keys" and you just get the permissions to see - so I added my admin account....
78814-image.png

and suddenly, the export is successful ?!?!?!!! so it's a permissions issue...


image.png (11.3 KiB)
· 3
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

Glad to hear that your issue was resolved. Please help to "accept answer" to end this thread up. Thank you.

0 Votes 0 ·

Can you expand on what you did when you say "checked the option 'manage private keys'"? Where are you doing that?

Thanks

0 Votes 0 ·

sorry, I missed your response, and I just noticed it. see below for the answer and the full working script.

0 Votes 0 ·
PrinsAndre-6100 avatar image
0 Votes"
PrinsAndre-6100 answered PrinsAndre-6100 edited

in MMC, certificates, select a certificate, right click on it:

101044-image.png

but more importantly, I realized that I am using in powershell the option -runas which means it runs as local administrator.

it is a bit complicated. I have to run powershell using run as different user, and use my admin account - because that account has rights on the certificate server to request a certificate.
but on my local machine I have to run some commands as local administrator.... so the the certificate is imported locally using local administrator -when I call the certreq.exe in line 100,104 and 110 I use -runas
i.e. line 100: Start-Process "certreq" -ArgumentList "-new $inf $req" -Verb "RunAs"
And when I am exporting it is with my admin account.

So I created a separate script to update the certificate permissions to grant my admin account rights


Main script:

 clear
 $servers = get-content "C:\Certificates\servers.txt"
 $outpath = "C:\Certificates"
 $password = "Password"
    
 $CAName = "certenroll.domain.com"
 $TemplateName = "operationsManagerCert"
 $E = "andre.prins@company.com"
 $OU = "CES"
 $O = "company"
 $L = "city"
 $S = "state"
 $C = "country"
    
    
 ##############################################################################
    
    
 function Remove-ReqTempfiles() {
     param(
         [String[]]$tempfiles
     )
     Write-Verbose "Cleanup temp files..."
     Remove-Item -Path $tempfiles -Force -ErrorAction SilentlyContinue
 }
    
 Function TestReq
 {
     $Done=$False
     Start-Sleep -Seconds 5
     do
     {
         $proc = Get-Process -Name certreq -ErrorAction SilentlyContinue
         if ($proc.count -ge 1)
         {start-sleep -Seconds 1}
         else
         {$Done = $true}
     } until ($Done)
 }
    
    
 ############################### Create Certificates and load in \LocalMachine\My ########################################################
    
 #get the CA details from AD
 $rootDSE = [System.DirectoryServices.DirectoryEntry]'LDAP://RootDSE'
 $searchBase = [System.DirectoryServices.DirectoryEntry]"LDAP://$($rootDSE.configurationNamingContext)"
 $CAs = [System.DirectoryServices.DirectorySearcher]::new($searchBase,'objectClass=pKIEnrollmentService').FindAll()
    
 if($CAs.Count -eq 4)
 {$CAName = "$($CAs[2].Properties.dnshostname)\$($CAs[2].Properties.cn)"}
 else 
 {$CAName = ""}
    
 if (!$CAName -eq "") 
 {$CAName = " -config `"$CAName`""}
    
 $server1 = @()
 foreach ($CN in $servers)
 {
     if ($CN -eq "" -or $CN -eq $Null) {continue}  #skip empty lines
     if ($CN.IndexOf(".") -gt 1)
     {
  $FriendlyName = $CN.Substring(0,$CN.IndexOf("."))
  $server1 += $CN.Substring(0,$CN.IndexOf("."))
  }
     else
     {
  $FriendlyName = $CN
  $server1 += $CN
  }
    
 $file = @"
 [NewRequest]
 Subject = "E=$E,CN=$CN,C=$c, S=$s, L=$l, O=$o, OU=$OU"
 MachineKeySet = TRUE
 UseExistingKeySet = False
 KeyLength = 2048
 KeySpec=1
 Exportable = TRUE
 RequestType = PKCS10
 ProviderName = "Microsoft RSA SChannel Cryptographic Provider"
 FriendlyName = "$FriendlyName"
 [RequestAttributes]
 CertificateTemplate = "$TemplateName"
 "@
    
     try
     {
    
         $inf = [System.IO.Path]::GetTempFileName()
         $req = [System.IO.Path]::GetTempFileName()
         $cer = Join-Path -Path $env:TEMP -ChildPath "$CN.cer"
         $rsp = Join-Path -Path $env:TEMP -ChildPath "$CN.rsp"
    
         Remove-ReqTempfiles -tempfiles $inf, $req, $cer , $rsp
         Set-Content -Path $inf -Value $file
    
         Write-host "generate .req file with certreq.exe"
         $error.Clear()
         Start-Process "certreq" -ArgumentList "-new $inf $req" -Verb "RunAs"
         TestReq
    
         Write-host "certreq -submit $CAName `"$req`" `"$cer`""
         Start-Process "certreq" -ArgumentList "-submit $CAName $req $cer" -Verb "RunAs"
         TestReq
    
         Write-host "request was successful. Result was saved to $cer"
    
         write-host "retrieve and install the certificate"
         Start-Process "certreq" -ArgumentList "-accept $cer -user" -Verb "RunAs"
    
         TestReq
    
         write-host "Done, cleaning up temp files"
         Remove-ReqTempfiles -tempfiles $inf, $req, $cer, $rsp
    
     }
     catch
     {
         write-host "Error during request"
         $error
     }
 }
    
    
 #######################################################################################
 # update security to allow $username to export the cert with private key - we have to use "RunAs" Administrator
 # so the easiest is to create a seperate script.
 # This script loads servers.txt and finds all the certificates in the store and updates security
    
 Start-Process PowerShell.exe -Verb "RunAs" -ArgumentList "C:\Certificates\UpdateSecurity.ps1" -Wait
    
 #######################################################################################
 Write-Host "Start exporting the certificates"
    
 $mypwd = ConvertTo-SecureString -String $password -Force -AsPlainText
 $certs = Get-ChildItem -Path cert:\LocalMachine\my    #currentuser  LocalMachine
 foreach ($cert in $certs)
 {
     if ($cert.Issuer -eq "CN=BHI WH Managed CA 1, DC=ent, DC=bhicorp, DC=com" -and $cert.FriendlyName -in $server1)
     {
         $name = $cert.DnsNameList.UniCode
         $CertFileName = $cert.FriendlyName
         $outname = $outpath + "\" + $CertFileName + ".pfx"
         Export-PfxCertificate -Cert $cert -FilePath $outname -Password $mypwd -ChainOption EndEntityCertOnly -NoProperties -Verbose
         write-host "Exported Certificate for " $cert.FriendlyName
     }
 }
    
    
 #Remove the certificates from the store once exported.
 Start-Process PowerShell.exe -Verb "RunAs" -ArgumentList "C:\Certificates\DeleteCerts.ps1" -Wait
 write-host "Finished"


this is the script UpdateSecurity.ps1 - it also reads the same servers.txt, so it exactly knows which certificates to update....

 $ErrorActionPreference="continue"
 $userName = "domain\Myadminaccount"
 $permission = "FullControl"
 $certStoreLocation = "\localmachine\My"
 $servers = get-content "C:\Certificates\servers.txt"
 $server1 = @()
 foreach ($S in $servers)
 {
     if ($S.IndexOf(".") -gt 1)
     {$server1 += $S.Substring(0,$S.IndexOf("."))}
     else
     {$server1 += $S}
 }
    
 $mypwd = ConvertTo-SecureString -String "Baker123" -Force -AsPlainText
 $certs = Get-ChildItem -Path cert:\LocalMachine\my    #currentuser  LocalMachine
 foreach ($cert in $certs)
 {
     if ($cert.Issuer -eq "CN=CERT CA 1, DC=Domain, DC=com" -and $cert.FriendlyName -in $server1)
     {
    
         $store = New-Object System.Security.Cryptography.X509Certificates.X509Store("My","LocalMachine")
         $store.Open("ReadWrite")
         $rwCert = $store.Certificates | where {$_.Thumbprint -eq $cert.Thumbprint}
         $rsaCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
         $fileName = $rsaCert.key.UniqueName
         $path = $env:ALLUSERSPROFILE + "\Microsoft\Crypto\RSA\MachineKeys\" + $fileName
         $permissions = Get-Acl -Path $path
         $rule = new-object security.accesscontrol.filesystemaccessrule $userName, $permission, allow
         $permissions.AddAccessRule($rule)
         Set-Acl -Path $path -AclObject $permissions
     }
 }


and to complete all the work, and delete the certificates at the end, DeleteCerts.ps1

 $servers = get-content "C:\Certificates\servers.txt"
 $certs = Get-ChildItem -Path cert:\LocalMachine\my    
 foreach ($CN in $servers)
 {
     if ($CN -eq "" -or $CN -eq $Null) {continue}  #skip empty lines
     if ($CN.IndexOf(".") -gt 1)
     {
  $server1 += $CN.Substring(0,$CN.IndexOf("."))
  }
     else
     {
  $server1 += $CN
  }
 }
    
 foreach ($cert in $certs)
 {
     if ($cert.Issuer -eq "CN=Certs CA 1, DC=Domain, DC=com" -and $cert.FriendlyName -in $server1)
     {
             $path = "Cert:\LocalMachine\my\" + $cert.Thumbprint
             Get-ChildItem $path | Remove-Item -Force
     }
 }


I run this now on a regular basis. all I do is populating servers.txt and run my script.

and to overcome the annoying UAC prompts when running the CertReq.exe as local administrator, I have 2 batchfiles to disable and enable the UAC:

disable:
REG ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System /v ConsentPromptBehaviorAdmin /t REG_DWORD /d 0 /f

Enable UAC:
REG ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System /v ConsentPromptBehaviorAdmin /t REG_DWORD /d 5 /f

and while writing this last bit, I realize, I can add that to the main script ;-)
so that's a last modification I will add myself, but you can figure that out yourself too :-)






image.png (62.0 KiB)
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.