PKCS #12 File Types: Portable Protected Keys in .NET

 

Michel I. Gallant, Ph.D.
JavaScience Consulting

March 2004

Applies to:
   Microsoft® .NET Framework version 1.1
   Microsoft® Visual Studio® .NET
   Microsoft® Windows® security

Summary: This article provides the code framework for accessing the certificates, public keys, and private keys within pfx/p12 files from .NET Framework code using P/Invoke to CryptoAPI. It requires only the .NET Framework version 1.1 with no additional support. (11 printed pages)

Download the PKCS.exe code sample.

Contents

Introduction
Using PFX with .NET
CryptoAPI Support for PKCS #12
Code Overview
Code Details
Removing Key Containers
Conclusion
References

Introduction

Digital signature generation and enveloped encryption using asymmetric keys in Windows most commonly involves the use of RSA key pairs stored and protected under installed CryptoAPI CSP (Cryptographic Service Providers). In EncryptTo/DecryptTo: Encryption in .NET with CryptoAPI Certificate Stores, we discussed RSA enveloping (encryption and decryption of secret, symmetric session keys). The RSA keys were obtained from the CryptoAPI key containers associated with installed certificates in CryptoAPI certificate stores. However, anyone who has purchased a certificate for SSL, secure e-mail, or code-signing usage from any of the well-known Certificate Authorities (CA) will be familiar with the Certificate Export Wizard, which optionally can include the associated private key into a password-protected file:

ms867088.pkcs12_01(en-us,MSDN.10).gif

Figure 1. Certificate Export Wizard

All Windows operating systems define the extensions .pfx and .p12 as Personal Information Exchange, or PKCS #12, file types. The default association for these file types raises the certificate install dialog using the internal cryptographic extensions API:

 rundll32.exe cryptext.dll,CryptExtAddPFX %1

Some of the most common reasons for exporting certificate keys to a PKCS #12 file format are:

  • Backing up public/private RSA asymmetric key pairs in safe offsite storage
  • Porting the RSA key pair to another computer for fixed installation, or temporary usage (e.g. code-signing)
  • Removing sensitive EFS (Encryption File System) recovery keys for backup

The Platform SDK (PSDK) security glossary defines PKCS #12 as:

The detailed specification for PKCS #12 is available from the RSA Security website and will not be discussed here. The Windows platform and CryptoAPI implements a subset of the full PKCS #12 specification.

This article provides the code framework for accessing the certificates, public, and private keys within pfx/p12 files from .NET Framework code using P/Invoke to CryptoAPI, and requires only the .NET Framework version 1.1 with no additional support. (From here on in, we will usually refer to any PKCS #12 file (pfx, p12 etc.) as PFX). The intent is to be able to access the protected PFX data, in a transient manner, from any computer, without permanently importing the keys into CryptoAPI-protected key storage. In this regard, the PFX file can be viewed as a kind of "soft" smart card, enabling RSA digital signature generation or decryption of enveloped content from any .NET Framework-enabled platform. Since PFX files are almost always quite small, typically less than 10 Kbytes, they are easily portable to any removable, recordable medium.

Using PFX with .NET

PFX files contain encrypted private keys, certificates, and potentially other secret information. To use PFX with .NET, we need to access the information in a form usable in .NET. This is conceptually similar to the approach used in the previous article, EncryptTo/DecryptTo: Encryption in .NET with CryptoAPI Certificate Stores, except here we derive the certificate and key credentials from a portable PFX file. To accomplish this, we will create a wrapper class, JavaScience.PfxOpen, which encapsulates:

  • Opening a PFX file and verifying that the file is a valid PFX file
  • Importing the PFX file into a temporary CryptoAPI memory store, and the associated private key(s) into a new key container under the default CSP
  • Enumerating all certificates in the new memory store, and finding the first certificate with an associated private key
  • Assigning fields for the CSP key container, provider name, type, and KeySpec
  • Assigning fields for the public certificate, RSA public key modulus, exponent, and key size
  • Providing a method to remove the key container, if required, after use

All the important fields required for cryptographic use from .NET are exposed as public GET properties. It is important to understand that we are not exposing the sensitive private key parameters directly in our class, but only acquiring the necessary properties, like key container names, that allow secure controlled access by the underlying CSP to the private keys. With access to the key container name holding the RSA private key, digital signature generation, or asymmetric decryption in .NET is possible with suitable initialization of an RSACryptoServiceProvider instance using the container property in the PfxOpen class:

  CspParameters cp = new CspParameters();
  cp.KeyContainerName = oPfxOpen.container;
  RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(cp);

Alternatively, we may only wish to use the imported public certificate properties from the PFX, for example, to RSA-encrypt to ourselves. In this case, we initialize an instance of RSACryptoServiceProvider using the PfxOpen class properties keyexponent and keymodulus:

  RSAParameters RSAKeyInfo   = new RSAParameters();
  RSAKeyInfo.Modulus   = oPfxOpen.keymodulus;
  RSAKeyInfo.Exponent   = oPfxOpen.keyexponent;
  RSACryptoServiceProvider oRSA   = new RSACryptoServiceProvider();
  oRSA.ImportParameters(RSAKeyInfo);

PfxOpen overrides the ToString() method to display all the PfxOpen instance properties, which is a useful feature for debugging.

Additionally, since PFX files are almost always password-protected, we create a JavaScience.PswdDialog class, which is a Windows Form dialog designed to retrieve the password from the user:

ms867088.pkcs12_02(en-us,MSDN.10).gif

Figure 2. PFX Password dialog

Since strings are immutable objects in .NET managed code, the password data is only removed from memory by the common language runtime (CLR) garbage collection process. Unfortunately, this means that password strings are vulnerable. Thus, managed password dialogs such as that shown in Figure 2 should only be used on trusted, secured computers. For further information on protecting private data in .NET, see "Protecting Secret Data" in Writing Secure Code.

It is possible to P/Invoke to the CredUI capability, but this functionality is only available for Windows XP and later versions.

CryptoAPI Support for PKCS #12

CryptoAPI has rather modest support for handling PFX files, but it is sufficient to enable .NET digital signatures and development. The only CryptoAPI PFX functions that are of interest here are:

  • PFXImportCertStore() to import a PFX file into a CryptoAPI transient memory store with the imported keys being managed by the default CSP
  • PFXIsPFXBlob() to validate a buffer as a valid PFX structure

It should be noted that CAPICOM 2 has good support for importing certificates and keys from PFX files into CAPICOM Certificates and Stores. However, we will not be using CAPICOM 2 here, though similar capability is available via CAPICOM.

Code Overview

Our PFX utility application consists of four classes, all in the JavaScience namespace:

  • PfxOpen: the main PFX decoding class discussed above
  • Win32: encapsulates the P/Invoke declarations for CryptoAPI functions and structures
  • PswdDialog: a Windows Form utility to retrieve user-supplied passwords
  • TextPFX: a test class to exercise PfxOpen

With the C# source code files and the password dialog box icon file in the same directory, the application can be compiled manually using the .NET Framework SDK 1.1 compiler (or via Visual Studio .NET) with:

csc.exe /m:JavaScience.TestPfx /res:keylock.ico,JavaScience.keylock.ico TestPfx.cs PFxOpen.cs PswdDialog.cs Win32.cs

This code creates a single file console application assembly, TestPfx.exe, that contains all classes. Note that we specify the /m (main entry point class) switch, since the class PswdDialog also contains a main entry point that is retained for stand-alone test purposes. Also, an icon resource, keylock.ico, is embedded into the assembly as a manifest resource. The application is executed from a command prompt:

TestPfx  [pfxfilename]  [pswd] 

If only a PFX filename is supplied, the user is prompted for a password via a PswdDialog. The TestPfx class:

  • Validates the supplied PFX file
  • Prompts the user for a password, if necessary
  • Attempts to open the PFX file and instantiates a PfxOpen instance
  • If successful, displays all PfxOpen properties using ToString()
  • Pauses for 2 seconds, representing some useful cryptographic usage of the PFX data
  • Attempts to remove the generated persistent key container and keys therein

In an extended implementation of the sample here, the public properties of the PfxOpen instance would be used to instantiate .NET asymmetric providers, as mentioned above, to actually use the imported keys for signing or encryption. A typical sample output for TestPfx shows the information acquired from the PFX file.

Code Details

Only code details not discussed in my previous article will be discussed here.

The instance method PfxOpen.LoadPfx(String pfxfilename, ref String pswd) loads the PFX binary file into byte[] pfxdata, which is then used to initialize a CRYPT_DATA_BLOB PFX blob struct, and memory is allocated:

 [StructLayout(LayoutKind.Sequential)]
  public struct CRYPT_DATA_BLOB
  {
   public int cbData;
   public IntPtr pbData;
  }
 ...

  CRYPT_DATA_BLOB ppfx = new CRYPT_DATA_BLOB();
  ppfx.cbData = pfxdata.Length;
  ppfx.pbData = Marshal.AllocHGlobal(pfxdata.Length);
  Marshal.Copy(pfxdata, 0, ppfx.pbData, pfxdata.Length);

After using Win32.PFXIsPFXBlob() to verify that we have a valid PFX blob, we attempt to import the PFX certificates into a transient CryptoAPI memory store, with the associated private keys placed in a newly created key container using Win32.PFXImportCertStore():

 [DllImport("crypt32.dll", SetLastError=true)]   
  public static extern IntPtr PFXImportCertStore(
   ref CRYPT_DATA_BLOB pPfx, 
   [MarshalAs(UnmanagedType.LPWStr)] String szPassword,
   uint dwFlags);
...
 hMemStore = Win32.PFXImportCertStore(ref ppfx, pswd, CRYPT_USER_KEYSET) ;
 pswd = null;
  if(hMemStore == IntPtr.Zero){
   string errormessage = new Win32Exception(Marshal.GetLastWin32Error()).Message;
   Console.WriteLine("\n{0}", errormessage);
   Marshal.FreeHGlobal(ppfx.pbData);
   return result;
  }
 Marshal.FreeHGlobal(ppfx.pbData);

We define CRYPT_DATA_BLOB as a managed structure that must be passed by reference to match the CryptoAPI function argument pointer CRYPT_DATA_BLOB* pPFX.

If PFXImportCertStore succeeds, we have an IntPtr handle to the memory store containing the imported certificates. We now release unmanaged memory assigned for the CRYPT_DATA_BLOB structure member. At this stage, persistent key containers have been generated. Note that while PKCS #12 supports containing more than one private key, but we will assume that there is only a single private key, with matching certificate in the PFX.

Next, we enumerate the certificates in the memory store using Win32.CertEnumCertificatesInStore(), checking each certificate to see if it has an associated private key using Win32.CertGetCertificateContextProperty() with dwPropID = CERT_KEY_PROV_INFO_PROP_ID. The first certificate with a matching private key is used to return an IntPtr representing a handle to a CRYPT_KEY_PROV_INFO structure. This IntPtr is marshaled to return the managed CRYPT_KEY_PROV_INFO structure. The structure members are parameters needed for private key access, and are assigned to the instance fields of PfxOpen:

 [StructLayout(LayoutKind.Sequential)]
  public struct CRYPT_KEY_PROV_INFO
  {
   [MarshalAs(UnmanagedType.LPWStr)] public String ContainerName;
   [MarshalAs(UnmanagedType.LPWStr)] public String ProvName;
   public uint ProvType;
   public uint Flags;
   public uint ProvParam;
   public IntPtr rgProvParam;
   public uint KeySpec;
  }
 ....

     if(Win32.CertGetCertificateContextProperty(hCertCntxt, CERT_KEY_PROV_INFO_PROP_ID, pProvInfo, ref provinfosize))
   {
    CRYPT_KEY_PROV_INFO ckinfo = (CRYPT_KEY_PROV_INFO)Marshal.PtrToStructure(pProvInfo, typeof(CRYPT_KEY_PROV_INFO));
    Marshal.FreeHGlobal(pProvInfo);

    this.pfxcontainer= ckinfo.ContainerName;
    this.pfxprovname = ckinfo.ProvName;
    this.pfxprovtype = ckinfo.ProvType;
    this.pfxkeyspec  = ckinfo.KeySpec;
    this.pfxcert     = new X509Certificate(hCertCntxt);
    if(!this.GetCertPublicKey(pfxcert))
      Console.WriteLine("Couldn't get certificate public key");
    result = true;
    break; 
       }

The retrieved certificate handle hCertCntxt is also used to instantiate a .NET X509Certificate object using the overloaded constructor pfxcert = new X509Certificate(hCertCntxt). The certificate is passed to PfxOpen.GetCertPublicKey() and the public key data is decoded from the certificate using Win32.CryptDecodeObject(), parsed and the public key modulus, exponent and key size are assigned to instance fields of PfxOpen. Finally, we must release all opened handles to unmanaged objects:

//-------  Clean Up  -----------
  if(pProvInfo != IntPtr.Zero)
   Marshal.FreeHGlobal(pProvInfo);
  if(hCertCntxt != IntPtr.Zero)
   Win32.CertFreeCertificateContext(hCertCntxt);
  if(hMemStore != IntPtr.Zero)
   Win32.CertCloseStore(hMemStore, 0) ;
  return result;

Removing Key Containers

The CryptoAPI function PFXImportCertStore() creates a temporary memory store, and also new default CSP key container(s). However, the key containers are persistent, even after the memory store is removed. Therefore, the key container(s) must be manually deleted after use, if key persistence is not required. PfxOpen provides the DeleteKeyContainer() function to facilitate removal of key containers and associated keys. This is implemented using CryptoAPI CryptAcquireContext() with the CRYPT_DELETEKEYSET flag:

 internal bool DeleteKeyContainer(String containername, String provname, uint provtype)
 {
  const uint CRYPT_DELETEKEYSET   = 0x00000010;
  IntPtr hCryptProv = IntPtr.Zero;
  if(containername == null || provname == null || provtype == 0)
   return false;
  if (Win32.CryptAcquireContext(ref hCryptProv, containername, provname, provtype, CRYPT_DELETEKEYSET)){
    return true;
   }
  else
   {
    PfxOpen.showWin32Error(Marshal.GetLastWin32Error());
    return false;
   }
 }

Note that CryptoAPI key containers can also be removed from purely managed code:

internal bool NetDeleteKeyContainer(String containername, String provname, int provtype){
   CspParameters cp = new CspParameters();
   cp.KeyContainerName = containername;
   cp.ProviderName = provname;
   cp.ProviderType = provtype;
   RSACryptoServiceProvider oRSA = new RSACryptoServiceProvider(cp);
   oRSA.PersistKeyInCsp = false;
   oRSA.Clear();
   return true;
 }

The CAPICOM handling of PFX files imported into memory stores is slightly different with regard to the key containers. According to the CAPICOM.Store.Load documentation:

If the Load method is called on a memory store, any key containers that are 
created will be deleted when the memory store is deleted. 

Thus, CAPICOM automatically manages the deletion of the key containers associated with PFX loaded to memory stores, unlike CryptoAPI PFXImportCertStore().

Conclusion

This article has provided the code framework required to use PKCS #12 pfx/p12 files from .NET Framework code using P/Invoke. Being able to port and use your private RSA keys provides some very interesting and cool possibilities. It is not difficult to merge the code presented here, with the enveloped encryption/decryption classes presented in my previous article.

Note   Using keys imported into memory stores from pfx files as discussed here exposes the private key material, making it vulnerable to memory monitors.

References

About the Author

Michel I. Gallant, Ph.D., has over 20 years experience in the telecommunications industry. He has worked as a senior photonic designer, and as a security analyst and architect in a major Canadian telecommunications corporation. He has extensive experience in code-signing and applied cryptography. He was awarded an MVP in Security for 2003. Michel lives in Ottawa, Canada and enjoys playing surf music, designing puzzles, and designing innovative electronic projects.