View All Certificates On Smart Card

11.6k views Asked by At

I am trying to create a script to remove all but the newest certificate from any given smart card (in the SC Reader at the time). This is something that I intend to be able to distribute to end users, so it should be self sufficient. My first issue is reading the certificates on the card. I do not want to affect any certificates not on the smart card, so I looked for solution that directly read from the card, and I found this gem:

How to enumerate all certificates on a smart card (PowerShell)

It's old, but it looks like it should do what I need. It really does seem to work in general but PowerShell ISE crashes when I get to the line:

$store = new-object System.Security.Cryptography.X509Certificates.X509Store($hwStore)

I can create a generic store which defaults to the 'My' store by excluding the ($hwStore) from that line without issues, but specifying that store reliably crashes my PowerShell ISE.

Here is the function from that site, the line I have issue with is near the bottom.

function Get-SCUserStore {
[string]$providerName ="Microsoft Base Smart Card Crypto Provider"
# import CrytoAPI from advapi32.dll
$signature = @"
[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
[return : MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptGetProvParam(
   IntPtr hProv,
   uint dwParam,
   byte[] pbProvData,
   ref uint pdwProvDataLen, 
   uint dwFlags); 

[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
[return : MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptDestroyKey(
   IntPtr hKey);   

[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
[return : MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptAcquireContext(
   ref IntPtr hProv,
   string pszContainer,
   string pszProvider,
   uint dwProvType,
   long dwFlags);

[DllImport("advapi32.dll", CharSet=CharSet.Auto)]
[return : MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptGetUserKey(
   IntPtr hProv, 
   uint dwKeySpec,
   ref IntPtr phUserKey);

[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptGetKeyParam(
   IntPtr hKey,
   uint dwParam,
   byte[] pbData,
   ref uint pdwDataLen,
   uint dwFlags);

[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
[return : MarshalAs(UnmanagedType.Bool)]

public static extern bool CryptReleaseContext(
   IntPtr hProv,
   uint dwFlags);
"@

$CryptoAPI = Add-Type -member $signature -name advapiUtils -Namespace CryptoAPI -passthru

# set some constants for CryptoAPI
$AT_KEYEXCHANGE = 1
$AT_SIGNATURE = 2
$PROV_RSA_FULL = 1
$KP_CERTIFICATE = 26
$PP_ENUMCONTAINERS = 2
$PP_CONTAINER = 6
$PP_USER_CERTSTORE = 42
$CRYPT_FIRST = 1
$CRYPT_NEXT = 2
$CRYPT_VERIFYCONTEXT = 0xF0000000

[System.IntPtr]$hProvParent=0
$contextRet = $CryptoAPI::CryptAcquireContext([ref]$hprovParent,$null,$providerName,$PROV_RSA_FULL,$CRYPT_VERIFYCONTEXT)

[Uint32]$pdwProvDataLen = 0
[byte[]]$pbProvData = $null
$GetProvParamRet = $CryptoAPI::CryptGetProvParam($hprovParent,$PP_CONTAINER,$pbProvData,[ref]$pdwProvDataLen,0)

if($pdwProvDataLen -gt 0) 
  {
    $ProvData = new-Object byte[] $pdwProvDataLen
    $GetKeyParamRet = $CryptoAPI::CryptGetProvParam($hprovParent,$PP_CONTAINER,$ProvData,[ref]$pdwProvDataLen,0)
   }

$enc = new-object System.Text.UTF8Encoding($null)
$keyContainer = $enc.GetString($ProvData)

 write-host " The Default User Key Container:" $keyContainer

[Uint32]$pdwProvDataLen = 0
[byte[]]$pbProvData = $null
$GetProvParamRet = $CryptoAPI::CryptGetProvParam($hprovParent,$PP_USER_CERTSTORE,$pbProvData,[ref]$pdwProvDataLen,0)
if($pdwProvDataLen -gt 0) 
  {
    $ProvData = new-Object byte[] $pdwProvDataLen
    $GetKeyParamRet = $CryptoAPI::CryptGetProvParam($hprovParent,$PP_USER_CERTSTORE,$ProvData,[ref]$pdwProvDataLen,0)
    [uint32]$provdataInt = [System.BitConverter]::ToUInt32($provdata,0)
    [System.IntPtr]$hwStore = $provdataInt
   }

 $store = new-object System.Security.Cryptography.X509Certificates.X509Store($hwStore)

# release smart card
$ReleaseContextRet = $CryptoAPI::CryptReleaseContext($hprovParent,0)

return $store
}

I don't have any experience with P/Invoke (I think I said that right), so I am unsure how to troubleshoot commands derived from things imported that way.

Edit: The providers that are listed by certutil -scinfo -silent are:

Microsoft Base Smart Card Crypto Provider
Microsoft Smart Card Key Storage Provider

I have tried both of those in the below script with the same end result. The second of which gives me � characters when the script tells me what my default user key container is, so I have a feeling that it is not correct.

I did also try the x86 version of PowerShell, as suggested by Vesper. The application does not crash, and it does return a valid store with my smart card's certificate(s) on it. Now the issue is that I can't send that out to users, because expecting them to be able to navigate to the x86 version of PowerShell and then run a script with it is like expecting my dog to make me waffles... I suppose it could happen, but more likely than not something will go wrong and I'll end up having to do it myself anyway.

Edit2: Ok, so I guess I'll force that part of the script to run in x86 mode. I will post an answer with my updated code and accept it. If @Vesper posts an answer about the 64/32 bit thing (hopefully with a hair more info) I will accept his answer so that he gets credit since his comment is what lead me to the solution.

4

There are 4 answers

1
Vesper On BEST ANSWER

So, the main problem is actually that you're linking an x86 DLL into a x64 Powershell process. You can check whether your Powershell process is x64 like here (by querying (Get-Process -Id $PID).StartInfo.EnvironmentVariables["PROCESSOR_ARCHITECTURE"]), and if an x64 Powershell detected, start manually a Powershell (x86) located at $env:windir\syswow64\WindowsPowerShell\v1.0\powershell.exe with the same script. To get the full name of the script, use $MyInvocation.MyCommand.Definition. If Powershell is detected as x86, you proceed with importing the type and run the enumeration. An example:

$Arch = (Get-Process -Id $PID).StartInfo.EnvironmentVariables["PROCESSOR_ARCHITECTURE"];
$Arch
if ($arch -eq "AMD64") {
    $here=$myinvocation.mycommand.definition
    "$here launched as $arch!"
    start-process C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -NoNewWindow -ArgumentList $here -wait
    return
}
"now running under x86"
0
TheMadTechnician On

So my solution was to check if the powershell session is running in 32 or 64 bit mode, and if it is running in 64 bit mode (most likely) then it will run the original script as a job using the -RunAs32 argument switch. If it's already running in 32 bit mode it will simply invoke the scriptblock in the current session. Final script to get certificates off a smart card (as an x509 Certificate Store) ended up being:

$RunAs32Bit = {
[string]$providerName ="Microsoft Base Smart Card Crypto Provider"
# import CrytoAPI from advapi32.dll
$signature = @"
[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
[return : MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptGetProvParam(
    IntPtr hProv,
    uint dwParam,
    byte[] pbProvData,
    ref uint pdwProvDataLen, 
    uint dwFlags); 

[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
[return : MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptDestroyKey(
    IntPtr hKey);   

[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
[return : MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptAcquireContext(
    ref IntPtr hProv,
    string pszContainer,
    string pszProvider,
    uint dwProvType,
    long dwFlags);

[DllImport("advapi32.dll", CharSet=CharSet.Auto)]
[return : MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptGetUserKey(
    IntPtr hProv, 
    uint dwKeySpec,
    ref IntPtr phUserKey);

[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptGetKeyParam(
    IntPtr hKey,
    uint dwParam,
    byte[] pbData,
    ref uint pdwDataLen,
    uint dwFlags);

[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
[return : MarshalAs(UnmanagedType.Bool)]

public static extern bool CryptReleaseContext(
    IntPtr hProv,
    uint dwFlags);
"@

$CryptoAPI = Add-Type -member $signature -name advapiUtils -Namespace CryptoAPI -passthru

# set some constants for CryptoAPI
$AT_KEYEXCHANGE = 1
$AT_SIGNATURE = 2
$PROV_RSA_FULL = 1
$KP_CERTIFICATE = 26
$PP_ENUMCONTAINERS = 2
$PP_CONTAINER = 6
$PP_USER_CERTSTORE = 42
$CRYPT_FIRST = 1
$CRYPT_NEXT = 2
$CRYPT_VERIFYCONTEXT = 0xF0000000

[System.IntPtr]$hProvParent=0
$contextRet = $CryptoAPI::CryptAcquireContext([ref]$hprovParent,$null,$providerName,$PROV_RSA_FULL,$CRYPT_VERIFYCONTEXT)

[Uint32]$pdwProvDataLen = 0
[byte[]]$pbProvData = $null
$GetProvParamRet = $CryptoAPI::CryptGetProvParam($hprovParent,$PP_CONTAINER,$pbProvData,[ref]$pdwProvDataLen,0)

if($pdwProvDataLen -gt 0) 
    {
    $ProvData = new-Object byte[] $pdwProvDataLen
    $GetKeyParamRet = $CryptoAPI::CryptGetProvParam($hprovParent,$PP_CONTAINER,$ProvData,[ref]$pdwProvDataLen,0)
    }

$enc = new-object System.Text.UTF8Encoding($null)
$keyContainer = $enc.GetString($ProvData)

    write-host " The Default User Key Container:" $keyContainer

[Uint32]$pdwProvDataLen = 0
[byte[]]$pbProvData = $null
$GetProvParamRet = $CryptoAPI::CryptGetProvParam($hprovParent,$PP_USER_CERTSTORE,$pbProvData,[ref]$pdwProvDataLen,0)
if($pdwProvDataLen -gt 0) 
    {
    $ProvData = new-Object byte[] $pdwProvDataLen
    $GetKeyParamRet = $CryptoAPI::CryptGetProvParam($hprovParent,$PP_USER_CERTSTORE,$ProvData,[ref]$pdwProvDataLen,0)
    [uint32]$provdataInt = [System.BitConverter]::ToUInt32($provdata,0)
    [System.IntPtr]$hwStore = $provdataInt
    }

    $store = new-object System.Security.Cryptography.X509Certificates.X509Store($hwStore)

# release smart card
$ReleaseContextRet = $CryptoAPI::CryptReleaseContext($hprovParent,0)

return $store
}

#Run the code in 32bit mode if PowerShell isn't already running in 32bit mode
If($env:PROCESSOR_ARCHITECTURE -ne "x86"){
    Write-Warning "Non-32bit architecture detected, collecting certificate information in separate 32bit process."
    $Job = Start-Job $RunAs32Bit -RunAs32
    $SCStore = $Job | Wait-Job | Receive-Job
}Else{
    $SCStore = $RunAs32Bit.Invoke()
}
0
Chris Gray On

I have been attempting to solve this same problem, and have come up with the following code. This is exactly what you have, with a couple of additions to deal with the 64-bit environment. This should do what you want without re-launching PowerShell as a 32-bit process.

function Get-SCUserStore {
    [CmdletBinding()]
    param(
          [string]$providerName ="Microsoft Base Smart Card Crypto Provider"
        )
    # import CrytoAPI from advapi32.dll
    $signature = @"
[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
[return : MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptGetProvParam(
    IntPtr hProv,
    uint dwParam,
    byte[] pbProvData,
    ref uint pdwProvDataLen, 
    uint dwFlags); 

[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
[return : MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptDestroyKey(
    IntPtr hKey);   

[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
[return : MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptAcquireContext(
    ref IntPtr hProv,
    string pszContainer,
    string pszProvider,
    uint dwProvType,
    long dwFlags);

[DllImport("advapi32.dll", CharSet=CharSet.Auto)]
[return : MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptGetUserKey(
    IntPtr hProv, 
    uint dwKeySpec,
    ref IntPtr phUserKey);

[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptGetKeyParam(
    IntPtr hKey,
    uint dwParam,
    byte[] pbData,
    ref uint pdwDataLen,
    uint dwFlags);

[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
[return : MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptReleaseContext(
    IntPtr hProv,
    uint dwFlags);
"@

    $CryptoAPI = Add-Type -member $signature -name advapiUtils -Namespace CryptoAPI -passthru

    # set some constants for CryptoAPI
    $AT_KEYEXCHANGE = 1
    $AT_SIGNATURE = 2
    $PROV_RSA_FULL = 1
    $KP_CERTIFICATE = 26
    $PP_ENUMCONTAINERS = 2
    $PP_CONTAINER = 6
    $PP_USER_CERTSTORE = 42
    $CRYPT_FIRST = 1
    $CRYPT_NEXT = 2
    $CRYPT_VERIFYCONTEXT = 0xF0000000


    [System.IntPtr]$hProvParent=0

    if([Environment]::Is64BitProcess) {
        [Uint64]$pdwProvDataLen = 0
    } else {
        [Uint32]$pdwProvDataLen = 0    
    }
    $contextRet = $CryptoAPI::CryptAcquireContext([ref]$hprovParent,$null,$providerName,$PROV_RSA_FULL,$CRYPT_VERIFYCONTEXT)

    [byte[]]$pbProvData = $null
    $GetProvParamRet = $CryptoAPI::CryptGetProvParam($hprovParent,$PP_CONTAINER,$pbProvData,[ref]$pdwProvDataLen,0)

    if($pdwProvDataLen -gt 0) 
    {
        $ProvData = new-Object byte[] $pdwProvDataLen
        $GetKeyParamRet = $CryptoAPI::CryptGetProvParam($hprovParent,$PP_CONTAINER,$ProvData,[ref]$pdwProvDataLen,0)
    }

    $enc = new-object System.Text.UTF8Encoding($null)
    $keyContainer = $enc.GetString($ProvData)

    Write-Verbose ("The Default User Key Container:{0}" -f $keyContainer)

    if([Environment]::Is64BitProcess) {
        [Uint64]$pdwProvDataLen = 0
    } else {
        [Uint32]$pdwProvDataLen = 0
    }

    [byte[]]$pbProvData = $null
    $GetProvParamRet = $CryptoAPI::CryptGetProvParam($hprovParent,$PP_USER_CERTSTORE,$pbProvData,[ref]$pdwProvDataLen,0)

    if($pdwProvDataLen -gt 0) 
    {
        $ProvData = new-Object byte[] $pdwProvDataLen
        $GetKeyParamRet = $CryptoAPI::CryptGetProvParam($hprovParent,$PP_USER_CERTSTORE,$ProvData,[ref]$pdwProvDataLen,0)

        if([Environment]::Is64BitProcess) {
            [UInt64]$provdataInt = [System.BitConverter]::ToUInt64($provdata,0)
            [System.IntPtr]$hwStore = [Long]$provdataInt
        } else {
            [UInt32]$provdataInt = [System.BitConverter]::ToUInt32($provdata,0)
            [System.IntPtr]$hwStore = $provdataInt
        }
    }

    $store = new-object System.Security.Cryptography.X509Certificates.X509Store($hwStore)

    # release smart card
    $ReleaseContextRet = $CryptoAPI::CryptReleaseContext($hprovParent,0)

    return $store
}

write-host ((get-WmiObject win32_PnPSignedDriver|where{$_.deviceID -like "*smartcard*"}).devicename) "reports the following certificates;" 

# returns System.Security.Cryptography.X509Certificates.X509Store object representing PP_USER_CERTSTORE on Smart Card
$SCcertStore = Get-SCuserSTore

# enumerate certificates
$SCcertStore.certificates
0
vikas pachisia On

A complete example to convert mstest coverage file into an xml file is provided below. This example includes passing of parameters and a way to identify the current script location.

<#
.SYNOPSIS
    Script to convert code coverage report into xml format that can then be published by external tools.

.DESCRIPTION
    Covering code coverage staistics as part of quality improvement initiatives .

    https://stackoverflow.com/questions/30215324/vstest-code-coverage-report-in-jenkins
#>
Param(
    [String] $InputCoveragePath =@("..\GeneratedFiles\Docs\Reports"),
    [String] $OutputCoverageFileExtension =@(".coveragexml"),
    [String] $CoverageAnalysisAssembly =@("Microsoft.VisualStudio.Coverage.Analysis.dll"),
    [String[]] $ExecutablePaths =@(""),
    [String[]] $SymbolPaths =@("")
)
    $ScriptLocation = Split-Path $script:MyInvocation.MyCommand.Path -Parent
    Write-Host $ScriptLocation
<#
    if(!(Test-Path "$OutputCoverageFile")){
        Write-Host "Creating empty coveragle file $OutputCoverageFile"
        New-Item "$OutputCoverageFile" -ItemType "file" 
    }
#>

$RunAs32Bit = {
    Param(
        [String] $InputCoveragePath =@("..\GeneratedFiles\Docs\Reports"),
        [String] $OutputCoverageFileExtension =@(".coveragexml"),
        [String] $CoverageAnalysisAssembly =@("Microsoft.VisualStudio.Coverage.Analysis.dll"),
        [String[]] $ExecutablePaths =@(""),
        [String[]] $SymbolPaths =@(""),
        [String] $ScriptLocation =@(".")
    )
    Write-Host "[CoverageConverter][Begin]: Coverage conversion started..."

    Write-Host "[CoverageConverter][InputCoveragePath]: $InputCoveragePath"
    Write-Host "[CoverageConverter][OutputCoverageFileExtension]: $OutputCoverageFileExtension"
    Write-Host "[CoverageConverter][CoverageAnalysisAssembly]: $CoverageAnalysisAssembly"
    Write-Host "[CoverageConverter][ExecutablePaths]: $ExecutablePaths"
    Write-Host "[CoverageConverter][SymbolPaths]: $SymbolPaths"
    Write-Host "[CoverageConverter][ScriptLocation]: $ScriptLocation"
    
    Import-Module -Force -Name (Join-Path "$ScriptLocation" "Utilities.psm1")
    Add-Type -path "$CoverageAnalysisAssembly"

    $Result = 0
    if($InputCoveragePath -and (Test-Path "$InputCoveragePath") )
    {
        [string[]] $coverageFiles = $(Get-ChildItem -Path $InputCoveragePath -Recurse -Include *coverage)
        
        @($coverageFiles) | ForEach-Object {
            $coverageFile = $_
            $coverageFileOut = (Join-Path -Path $(Split-Path $_ -Parent) -ChildPath  ($(Get-Item $_).BaseName + "$OutputCoverageFileExtension"))

            Write-Host "If all OK the xml will be written to: $coverageFileOut"

            $info = [Microsoft.VisualStudio.Coverage.Analysis.CoverageInfo]::CreateFromFile($coverageFile, $ExecutablePaths, $SymbolPaths);
            if($info){
                $data = $info.BuildDataSet()
                $data.WriteXml($coverageFileOut)
            }
        }
    }
    else
    {
        Write-Host "Please specify a valid input coverage file."
        $Result = 1
    }

    Write-Host "[CoverageConverter][End]: Coverage conversion completed with result $Result"
    return $Result  
}

#Run the code in 32bit mode if PowerShell isn't already running in 32bit mode
If($env:PROCESSOR_ARCHITECTURE -ne "x86"){
    Write-Warning "Non-32bit architecture detected, processing original request in separate 32bit process."
    $Job = Start-Job $RunAs32Bit -RunAs32 -ArgumentList ($InputCoveragePath, $OutputCoverageFileExtension, $CoverageAnalysisAssembly, $ExecutablePaths, $SymbolPaths, $ScriptLocation)
    $Result = $Job | Wait-Job | Receive-Job
}Else{
    $Result = Invoke-Command -ScriptBlock $RunAs32Bit -ArgumentList ($InputCoveragePath, $OutputCoverageFileExtension, $CoverageAnalysisAssembly, $ExecutablePaths, $SymbolPaths, $ScriptLocation)
}