Extending .NET Cryptography with CAPICOM and P/Invoke
Michel I. Gallant, Ph.D.
JavaScience Consulting
June 2003
Applies to:
Microsoft® .NET Framework version 1.1
COM Interop and CAPICOM
P/Invoke and CryptoAPI native libraries
Summary: How to supplement the cryptographic capabilities currently shipping with the Microsoft .NET Framework version 1.1 through the use of COM interop and P/Invoke to CryptoAPI native libraries. (21 printed pages)
Contents
Introduction
CAPICOM: CryptoAPI Made Easy
.NET Cryptography: The Early Days
.NET and CAPICOM Interop
.NET and CryptoAPI via P/Invoke: Part 1
.NET and CryptoAPI via P/Invoke: Part 2
Conclusion
References
Introduction
As the Internet continues to grow, and as client and Web applications become more complex and distributed, the requirement of protecting information exchanged electronically or stored as persistent data files becomes ever more important. The Internet today is viewed by most as a "hostile environment", with hackers and crackers lurking nearby. Furthermore, as e-commerce becomes ubiquitous, the protection of the privacy of personal information shuttled about in network sessions is essential. The application of cryptography to the protection of electronic data is recognized as a viable approach for ensuring:
- Data integrity
- Data privacy
- Data authentication
However, it must be stressed that building applications using cryptography does not mean that your applications are secure. Implementing "crypto" in code must be accompanied with sound security practices including, but not limited to, appropriate selection and protection of secrets (passwords). Writing Secure Code (Second Edition, Microsoft® Press, 2003) serves as an excellent introduction to secure and practical guidelines for coding cryptography, particularly for Windows®.
Foundations of cryptography are based on well-understood and validated cryptographic algorithms for encryption, digital signature generation, hashing (message digests), and associated structures such as X.509 certificates, CMS (Cryptographic Message Syntax), and PKCS#7 structures, a subset of CMS. CMS is commonly used as the format for signing and encrypting messages and data, for example in applications such as S-MIME (Secure/Multipurpose Internet Mail Extensions) e-mail clients. Probably the best-known application of cryptography today is the use of the SSL (Secure Sockets Layer) network protocol to authenticate secure e-commerce Web sites to users, and to encrypt private data content transmitted between users and the Web sites. Success of SSL depends on the existence of a basic Public Key Infrastructure (PKI), the infrastructure responsible for issuance, verification, and tracking of digital certificates/keys required for implementing the secure connection. The end-user acceptance of SSL for e-commerce is no doubt related to the simplicity of use: the implementation is almost completely transparent to the end user, except for a status icon, most commonly a lock on the Web browser frame:
Figure 1. Status icon on Web browser
Less successful to date has been the widespread adoption of secure S/MIME e-mail, even though the cryptography infrastructure and algorithms used are very similar to that used in SSL. However, as users become more aware of the vulnerability of e-mail interception, and as the S/MIME e-mail clients become easier to use, it is likely S/MIME will become more widely used.
The core cryptography functionality in Windows is provided by CryptoAPI, which is implemented as a set of native libraries. CryptoAPI uses installed Cryptographic Service Provider (CSP) modules to implement the cryptographic algorithms (encryption, hashing, signing, etc.) and to securely manage and access key container databases. Developers invoke functionality implemented by CSPs using the exposed CryptoAPI function libraries. CryptoAPI enables applications developers to develop certificate and messaging applications without needing to understand the technical and mathematical details of the underlying algorithms. Certificates in Windows are maintained in CryptoAPI-managed named Certificate Stores (MY, AddressBook, Root, etc.) that are organized according to the intended use. CryptoAPI represents a broad and detailed framework for achieving many common cryptographic tasks, including key generation and maintenance, data encryption and enveloping, digital signatures, hashes, X.509 certificate store operations, and certificate validation. The breadth of this capability is shown in the CryptoAPI documentation on MSDN.
CAPICOM: CryptoAPI Made Easy
Successful application of even the most basic cryptographic tasks using CryptoAPI typically requires a good working knowledge of the Windows API and C/C++. A considerable learning curve is required to become acquainted with the key cryptographic concepts such as encodings (ASN.1 Abstract Syntax Notation) and data structures required as arguments by many CryptoAPI functions. Since the introduction of CryptoAPI in 1996, cryptographic functionality has been available with Windows NT®, Windows 95, and later Windows operating systems. However, as application of cryptography inevitably becomes more widespread, it is important that access to programming cryptography in Windows be available to developers with a broader range of backgrounds (Visual Basic®, Windows Script Host, .NET-compatible languages, etc.). CAPICOM was introduced in 2001 and was designed to enable easier access to underlying CryptoAPI functionality, particularly tasks associated with certificates. Visual Basic and Windows Script Host (Visual Basic Scripting Edition, JScript®) scripting programmers can now develop applications and utilities leveraging CryptoAPI, and often accomplish the more important cryptographic tasks with much less code than required for the identical task coded directly to CryptoAPI in C/C++. For example, the following simple code generates and displays a CMS-enveloped message in base64 encoding:
Set oEnvData = CreateObject("CAPICOM.EnvelopedData")
oEnvData.Content = "Your salary increase: 2000 Loonies"
Message = oEnvData.Encrypt
WScript.Echo "Your enveloped message" & vbCrLF & Message
A CAPICOM.EnvelopedData object is instantiated, the Content property is initialized with simple string data, and the Encrypt() method is called on the EnvelopedData object using default parameters. A random secret symmetric key is automatically generated with a default encryption algorithm and key size. The secret key is used to bulk-encrypt the content, which could represent binary file data. The AddressBook certificate store (named "Other People" certificates in Internet Explorer) dialog is presented to the user to select a recipient for the enveloped message. The recipient's RSA public key is automatically extracted from the selected certificate and is used to encrypt the secret symmetric key as diagramed below:
Figure 2. Symmetric key encryption diagram
This simple example demonstrates how CAPICOM encapsulates a great deal of the underlying CryptoAPI complexity. This is evident after a cursory inspection of the CryptoAPI documentation on Encoding Enveloped Data. However, CAPICOM does expose sufficient capability, in properties and methods, to adjust important cryptographic parameters. For example, in the example above, it may be desirable to change the symmetric key algorithm and strength; as a power user on Windows 2000, my system defaults to RC2 56-bit encryption strength, which today is considered to be poor encryption strength! Moore's Law indicates that computing power doubles every couple of years. Since the effectiveness of a cryptographic algorithm is effectively based on its computational complexity, its "effective strength" changes as computers get faster. For example, when Windows 2000 first shipped, 56-bit key size was considered adequate for many purposes. It is good practice to manually set the encryption algorithm and key length to be used when encrypting data. Starting with Windows XP, support for the new A.E.S. algorithm was added and should be considered if recipients have support for A.E.S. The EnvelopedData.Algorithm property allows full specification of the algorithm name and key size (including A.E.S. Advanced Encryption Standard/Rijndael on Windows XP+ systems). Further, the recipients can be specified with the EnvelopedData.Recipients property, representing a collection of recipient certificates, instead of using the default underlying system store dialog.
With the introduction of CAPICOM v2 in 2002, a very impressive subset of CryptoAPI functionality has been exposed, including easy and powerful certificate store search functions, support for CMS/PKCS#7-enveloped messages and even support for Authenticode signatures, an important Microsoft technology based on digital signatures. Authenticode technology provides support for code authentication during downloading (via Internet Explorer), control over what code can execute (e.g., ActiveX® controls, .NET assemblies with access security based on publisher condition), and control over who can extend applications (ActiveX controls, signed CSPs). For details on CAPICOM capability, consult the CAPICOM Reference. Very useful and detailed code samples in several languages are provided with the CAPICOM 2 distribution.
.NET Cryptography: The Early Days
The introduction of .NET technology promises to be the foundation for building powerful and secure object-orientated distributed Web applications and services. Cryptography support, which is similar in .NET Framework 1.0 and 1.1, is encapsulated within these namespaces:
- System.Security.Cryptography
- System.Security.Cryptography.Xml
- System.Security.Cryptography.X509Certificates
Notable in .NET architecture is the stream-based design for symmetric encryption and hashing, which is consistent and easily interoperable with file and network streaming. The .NET CryptoStream class is the "mission-central" class for symmetric streaming encryption and has been discussed adequately in the .NET reference documentation and in the article, Protect Private Data with the Cryptography Namespaces of the .NET Framework, which focused mainly on the cryptography namespace inheritance structure and symmetric encryption algorithms. The inheritance structure of the various classes provides a logical and powerful design. Concrete implementations of the most important symmetric and asymmetric encryption algorithms and hashing algorithms are included. Currently, most of these classes are managed wrapper classes for underlying CryptoAPI implementations, ensuring stability and leveraging FIPS-140 certified capability. The exceptions, such as SHA256Managed and RijndaelManaged, are identified by the "Managed" suffix. These managed classes usually represent classes not implemented in CryptoAPI. A notable "secure by default" feature of the .NET cryptography classes is that default constructors for symmetric and asymmetric encryption algorithm classes automatically generate cryptographically random keys with strong parameters (if possible and available). For example, on my Windows 2000 system, the following code generates a new transient RC2 session key with a key size of 128 bits and CBC mode:
RC2CryptoServiceProvider rc2 = new RC2CryptoServiceProvider() ;
Console.WriteLine("Key size: {0}", rc2.KeySize);
Console.WriteLine("Mode: {0}", rc2.Mode) ;
Note that this is stronger encryption than that selected by default using CAPICOM and EnvelopedData (56-bit) as described above. It is important when using cryptography that developers ensure that keys generated have sufficient encryption strength for the intended purpose, particularly if default constructors are used that may mask these important details. Implementations of asymmetric algorithms such as RSACryptoServiceProvider include support for persisting and reuse of associated public/private keys in standard CryptoAPI key containers. A CryptoAPI key container name is a unique name generated by a cryptographic service provider that can be used to reference a specific key. For example, the following code will instantiate an RSA provider class using keys associated with an existing key container. The key container and keys might have been previously generated with any of (1) CryptoAPI directly, (2) the Makecert.exe Platform SDK tool, or (3) the RSACryptoServiceProvider constructor below. If the named key container does not already exist, a new key container is created and a new random asymmetric public/private key pair is generated and populates the new container:
const string mykeycontainer = "{D88B08F4-xxxx-xxxx-xxxx-xxxxx9537507}";
CspParameters cp = new CspParameters();
cp.KeyContainerName = mykeycontainer;
RSACryptoServiceProvider RSAprov = new RSACryptoServiceProvider(cp);
If this RSA provider is used to digitally sign data using the RSAPKCS1SignatureFormatter.CreateSignature() method, and if the private key has strong password protection, then a native password dialog will be presented for access to the private key in this container.
Inclusion of XML signature support in .NET Framework underpins the importance of XML as an evolving standard in Web applications and the importance of authenticated documents exchanged over networks. For further details on cryptography capability supported by the current version of .NET Framework, see the Cryptography Overview in the .NET Framework Developer's Guide.
A key component of PKI and cryptographic trust and authentication infrastructure is the use of industry-standard X.509 v3 certificates, validated, signed, and issued by trusted commercial CAs (Certificate Authorities). Version 1.1 of the .NET Framework provides basic support for instantiating and parsing X.509 certificates. The overloaded X509Certificate constructor accepts arguments of a certificate file path, a byte array representing the certificate, or an IntPtr representing a CryptoAPI certificate handle (PCERT_CONTEXT). Note that currently, only certificates in binary DER format are supported. Certificate files encoded as base64 must be decoded first to binary DER format. Base64 is an encoding format that maps a consecutive 6 bits of arbitrary binary data into a printable character (3 source bytes encoded to 4 character bytes) for transmission by electronic mail. A noticeable omission in certificate support in .NET Framework is any direct support for accessing or manipulation of CryptoAPI certificate stores, functional organizations of users, and machine certificates. For developing applications using Windows certificate stores, it is desirable to be able to browse for certificates in various stores, and to directly instantiate a certificate object (for an enveloped recipient, for example) from a specific certificate store. Also, the .NET Framework currently does not provide support for generating or verifying CMS/PKCS#7 signature objects or Authenticode signatures. While there are indications that broader support for certificate stores and PKCS#7 will be available in future releases of the .NET Framework, it is useful to understand how to achieve basic system certificate-store access from managed code in order to target the .NET Framework 1.0/1.1 today.
The remainder of this article demonstrates how to supplement the existing .NET Framework 1.1 capabilities through the rich interoperability capabilities provided by the .NET Framework. Specifically, the samples provided demonstrate:
COM interop with CAPICOM
Platform Invoke (P/Invoke) with native CryptoAPI libraries.
From a security perspective, use of COM interop and P/Invoke involves accessing native code, and therefore requires extra care in ensuring that security holes are not exposed in accessing these native methods from managed code. Remember that any .NET application accessing native code requires the FullTrust permission set. Depending on your deployment requirements, follow the appropriate secure coding guidelines for the .NET Framework.
.NET and CAPICOM Interop
As already mentioned, much of the useful functionality of CryptoAPI has been encapsulated in CAPICOM. CAPICOM provides advanced capability, including support for CMS signatures, enveloped messages, encryption, hashing, cryptographically strong random numbers, Authenticode signatures, and advanced certificate store access. The latest release of CAPICOM is available for download. As a general recommendation for .NET developers building applications using cryptography, if the required functionality is not available natively in the core .NET classes, one should consider using CAPICOM as a first approach. If the functionality is not provided by CAPICOM, then one might consider using P/Invoke with CryptoAPI libraries directly to access the required feature. Alternatively, third-party managed libraries may be available.
To use CAPICOM—a standard COM component—from .NET, the preferred approach is to import the entire type library using the .NET Type Library Importer tool tlbimp.exe, which generates a COM interop assembly, which is a bridge into the native COM library. Alternatively, COM definitions can be directly embedded into the .NET source code using COM interop attributes. This approach requires a deeper understanding and knowledge of the COM types and is typically only used if some type definitions generated by tlbimp need to be modified, or if the interop assembly generated by tlbimp is too large. Therefore, only the tblimp.exe approach will be discussed here, and is desirable for library reuse purposes as discussed below. In summary, to use CAPICOM with .NET requires:
- Generation of a CAPICOM interop assembly
- Native capicom.dll installation and registration
- Deployment of a .NET interop assembly along with the application assembly
A private interop assembly for CAPICOM can be generated manually using:
tlbimp capicom.dll /namespace:CAPICOM /out:Interop.CAPICOM.dll
while a strong-named interop assembly is generated using:
tlbimp capicom.dll /namespace:CAPICOM /out:Interop.CAPICOM.dll /keycontainer:myContainer
where myContainer is any CryptoAPI key container name. The key container name could be associated with an existing certificate, for which an associated private key is available, or the container name could be generated by importing (into CryptoAPI) from a keyfile generated by the Strong Name utility SN.exe:
sn -k "strongpair.snk"
sn -i "strongpair.snk" "myContainer"
Alternatively, the tlbimp ../keyfile option can be used to specify the key file generated by SN.exe. This is essentially the same command executed by Visual Studio® .NET when a COM reference is added to a project using the Projects | Add Reference ... | COM menu. Note that the interop assembly has explicitly been assigned the namespace CAPICOM while the assembly is named Interop.CAPICOM. It is desirable to name the assembly differently than the original COM library to avoid file name clashes. Details of the methods and property signatures, as described by the interop assembly metadata, can be conveniently viewed with the Intermediate Language Disassembly tool Ildasm.exe. Using .NET Framework 1.0 with CAPICOM 2.0.0.3 generates an interop assembly with default assembly version 2.0.0.0.
The simple C# application below is compiled referencing the Interop.CAPICOM assembly using:
csc /r:interop.capicom.dll FindCertnet.cs
To execute the application, the Interop.CAPICOM.dll assembly can be deployed to the application directory as a private assembly, or if the interop assembly is strong-named, it can be deployed to the Global Assembly Cache (GAC) of the target computer(s) for shared access by other .NET-connected applications referencing CAPICOM. It is easy with Visual Studio .NET to build a deployment project to install a shared library to the client machine GAC as described in the Knowledge Base article, HOW TO: Deploy an Assembly to the Target Computer Global Assembly Cache.
The console application below demonstrates basic system certificate store access using CAPICOM; this functionality is not currently available in the .NET Framework classes. It also demonstrates different ways of using a Certificates collection, including the indexer. Note that the interop library exposes several CAPICOM enumerations, such as CAPICOM_STORE_LOCATION. Casting a Certificate instance to the ICertContext interface provides the context or certificate handle that can be used with the overloaded X509Certificate(IntPtr handle) constructor, although with CAPICOM installed, all the functionality is available from the interop assembly. Note also that the Certificates indexer property, as a Visual Basic component, starts at index 1. A few properties of an X509Certificate object are displayed:
using System;
using System.Collections;
using System.Security.Cryptography.X509Certificates;
using CAPICOM;
class FindCertnet
{
static private String storeName = "My";
static StoreClass oStore;
static Certificates oCerts;
static String filter = "yoursearchstring";
[STAThread]
static void Main(string[] args)
{
oStore = new StoreClass();
oStore.Open(
CAPICOM_STORE_LOCATION.CAPICOM_CURRENT_USER_STORE,
storeName,CAPICOM_STORE_OPEN_MODE.CAPICOM_STORE_OPEN_EXISTING_ONLY |
CAPICOM_STORE_OPEN_MODE.CAPICOM_STORE_OPEN_READ_ONLY
);
oCerts = (Certificates)oStore.Certificates;
oCerts = (Certificates)oCerts.Find(
CAPICOM_CERTIFICATE_FIND_TYPE.CAPICOM_CERTIFICATE_FIND_SUBJECT_NAME,filter, false);
Console.WriteLine("\n{0} certificates match substring \"{1}\"", oCerts.Count, filter
);
foreach(Certificate ocert in oCerts) //Certificates is IEnumerable
ocert.Display() ;
//---- alernate equivalent way to enumerate ----------
//IEnumerator certenum = oCerts.GetEnumerator() ;
//while(certenum.MoveNext()){
// Certificate ocert = (Certificate) certenum.Current;
// ocert.Display();
//}
//--- get first cert using indexer, retrieve ICertContext and use from managed code -----
if(oCerts.Count>0){
Certificate firstcert = (Certificate)oCerts[1] ;
firstcert.Display() ;
ICertContext iCertCntxt = (ICertContext) firstcert;
int certcntxt = iCertCntxt.CertContext ;
IntPtr hCertCntxt = new IntPtr(certcntxt);
if(hCertCntxt != IntPtr.Zero)
{ //use certcontext from managed code
Console.WriteLine("CertContext:\t0x{0:X}", hCertCntxt.ToInt32()) ;
X509Certificate foundcert = new X509Certificate(hCertCntxt);
Console.WriteLine("\nFound certificate with SubjectName string \"{0}\"",filter);
Console.WriteLine("SubjectName:\t{0}", foundcert.GetName());
Console.WriteLine("Serial No:\t{0}", foundcert.GetSerialNumberString());
Console.WriteLine("HashString:\t{0}" , foundcert.GetCertHashString());
}
}
}
}
This example has barely scratched the surface of the powerful cryptographic functionality available using the .NET CAPICOM interop approach. Given that the current CAPICOM 2.0.0.3 installer .cab file is a mere 164 KB and the associated interop assembly generated by tlbimp is even smaller at 76 KB, deployment of the required components for this approach is modest indeed.
.NET and CryptoAPI via P/Invoke: Part 1
It is interesting and instructive to explore how to implement the previous certificate store access example using P/Invoke to directly access the native CryptoAPI libraries. In Part 2, a second example requires the use of P/Invoke. The advantage of invoking CryptoAPI functions directly using P/Invoke is that it does not require any extra component installation for deployment. However, the requirements section of the function documentation should be consulted to ensure the functionality is supported on the targeted platforms. As pointed out, in most cases P/Invoke should be considered as a last resort for implementing functionality not available in the .NET Framework classes, or via CAPICOM. Many of the CryptoAPI functions have several arguments with a significant level of complexity, leading to the possibility of coding errors by developers not familiar with CryptoAPI. However, in some cases this is the only solution, as the second example will demonstrate. Our first example essentially repeats the functionality of retrieving a handle to a certificate store, and searching for a specific certificate. In order to achieve this using P/Invoke, several CryptoAPI unmanaged native functions must be declared and prototyped for access by .NET managed code. For convenience the CryptoAPI function declarations are included in a WinCapi class. In practice such a class might contain a large number of managed function prototypes for unmanaged native functions and could be deployed to the GAC as a shared assembly library. One of the requirements in using P/Invoke with CryptoAPI is an understanding of the many constants defined mainly in the WinCrypt.h header file, available with the Platform SDK, or with Visual Studio. In .NET code, it is convenient and good practice to define required constants with identical names to those in the corresponding WinCrypt.h file. The constants could be included in the WinCapi wrapper class, or as constants in the application class, which is the approach I have used here. This also facilitates reading documentation for the CryptoAPI functions. Since many of the CryptoAPI functions have parameters or flags that affect the meaning of other arguments in the same function, it may be necessary to define several overloaded prototypes to describe functionality supported by one CryptoAPI function. The Platform Invoke Tutorial and documentation on marshalling of argument types should be consulted for details on defining the prototype functions.
The basic steps in our first P/Invoke sample are:
- Open the "MY" system certificate store using CertOpenSystemStore() specifying the default CSP
- Search for a certificate based on a substring of the SubjectName using CertFindCertificateInStore()
- Instantiate a managed X509Certificate certificate using an IntPtr.
- Free the certificate store and certificate contexts
using System;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.ComponentModel;
public class WinCapi {
/*
HCERTSTORE WINAPI CertOpenSystemStore(HCRYPTPROV hprov, LPTCSTR szSubsystemProtocol);
BOOL WINAPI CertCloseStore(HCERTSTORE hCertStore, DWORD dwFlags);
PCCERT_CONTEXT WINAPI CertFindCertificateInStore(
HCERTSTORE hCertStore,
DWORD dwCertEncodingType,
DWORD dwFindFlags,
DWORD dwFindType,
const void* pvFindPara,
PCCERT_CONTEXT pPrevCertContext);
BOOL WINAPI CertFreeCertificateContext(
PCCERT_CONTEXT pCertContext
);
*/
[DllImport("crypt32.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern IntPtr CertOpenSystemStore(
IntPtr hCryptProv,
string storename) ;
[DllImport("crypt32.dll", SetLastError=true)]
public static extern bool CertCloseStore(
IntPtr hCertStore,
uint dwFlags) ;
[DllImport("crypt32.dll", SetLastError=true)]
public static extern IntPtr CertFindCertificateInStore(
IntPtr hCertStore,
uint dwCertEncodingType,
uint dwFindFlags,
uint dwFindType,
[In, MarshalAs(UnmanagedType.LPWStr)]String pszFindString,
IntPtr pPrevCertCntxt) ;
[DllImport("crypt32.dll", SetLastError=true)]
public static extern bool CertFreeCertificateContext(
IntPtr hCertStore) ;
}
public class SimpleCert {
const string MY = "MY";
const string OTHERS = "AddressBook";
const uint PKCS_7_ASN_ENCODING = 0x00010000;
const uint X509_ASN_ENCODING = 0x00000001;
const uint CERT_FIND_SUBJECT_STR = 0x00080007;
static uint MY_ENCODING_TYPE = PKCS_7_ASN_ENCODING | X509_ASN_ENCODING ;
static string lpszCertSubject = "yoursearchstring" ;
public static void Main(){
IntPtr hSysStore = IntPtr.Zero;
IntPtr hCertCntxt = IntPtr.Zero;
hSysStore = WinCapi.CertOpenSystemStore(IntPtr.Zero, MY) ;
Console.WriteLine("Store Handle:\t0x{0:X}", hSysStore.ToInt32());
if(hSysStore != IntPtr.Zero)
{
hCertCntxt=WinCapi.CertFindCertificateInStore(
hSysStore,
MY_ENCODING_TYPE,
0,
CERT_FIND_SUBJECT_STR,
lpszCertSubject ,
IntPtr.Zero) ;
if(hCertCntxt != IntPtr.Zero){ //use certcontext from managed code
Console.WriteLine("CertContext:\t0x{0:X}", hCertCntxt.ToInt32()) ;
X509Certificate foundcert = new X509Certificate(hCertCntxt);
Console.WriteLine("\nFound certificate with SubjectName string \"{0}\"",lpszCertSubject);
Console.WriteLine("SubjectName:\t{0}", foundcert.GetName());
Console.WriteLine("Serial No:\t{0}", foundcert.GetSerialNumberString());
Console.WriteLine("HashString:\t{0}" , foundcert.GetCertHashString());
}
else
Console.WriteLine("Could not find SubjectName containing string \"{0}\"", lpszCertSubject);
}
//------- Clean Up -----------
if(hCertCntxt != IntPtr.Zero)
WinCapi.CertFreeCertificateContext(hCertCntxt);
if(hSysStore != IntPtr.Zero)
WinCapi.CertCloseStore(hSysStore, 0) ;
}
}
Note that CertFindCertificateInStore() does not automatically validate the certificate chain, if any, associated with a certificate returned by this function. This is a common mistake that can lead to security issues. CertGetCertificateChain() provides certificate chain validation functionality. It is important to free certificate contexts and close system stores when using P/Invoke. Note that setting the SetLastError named property in the DllImportAttribute makes it possible to track any errors returned by CryptoAPI. Explicit error checking has not been included here in order to simplify the code. CertOpenSystemStore() is a simplified function, which serves the common purpose of simply opening a specified system store. For more advanced certificate store access via P/Invoke (including memory stores, deleting stores, etc.), the CryptoAPI function CertOpenStore() can be used. A typical managed prototype (for C#) might be:
[DllImport("crypt32.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern IntPtr CertOpenStore(
IntPtr storeProvider,
uint dwMsgAndCertEncodingType,
IntPtr hCryptProv,
uint dwFlags,
String cchNameString) ;
CertFindCertificateInStore() has comprehensive support and options for searching all types of CryptoAPI certificate stores. Consult the CryptoAPI documentation for details.
.NET and CryptoAPI via P/Invoke: Part 2
As a final example of .NET and P/Invoke, recall that the .NET RSACryptoServiceProvider asymmetric algorithm provider can be instantiated with a CspParameters argument which can be used to specify the use of an existing CryptoAPI key container. However, if I wish to know what my key containers actually are, the ability to enumerate the existing persistent key containers is not provided with current .NET cryptography classes, nor is this functionality available in CAPICOM. The powerful CryptoAPI function CryptGetProvParam() retrieves various properties associated with a cryptographic service provider. The original CryptoAPI function declaration is included as commented text for discussion purposes. The example below acquires a CSP handle using the "safe" CRYPT_VERIFYCONTEXT flag, since explicit private key access is not required for enumeration of key containers. (By default, keys associated with the current user are queried. If Local Machine keys/containers are of interest, the flag should specify the combination: CRYPT_VERIFYCONTEXT | CRYPT_MACHINE_KEYSET). The query parameter PP_ENUMCONTAINERS is passed to CryptGetProvParam() specifying key container enumeration. For demonstration purposes, overloaded prototypes for CryptGetProvParam() are declared, showing two different ways of receiving string buffer data returned by the pbData argument of this function. Note that for PP_ENUMCONTAINERS, the container names are returned as null terminated CHAR strings, i.e. as ANSI bytes. Thus, one of the prototypes explicitly marshals the data as [MarshalAs(UnmanagedType.LPStr)]. Also, for this enumeration the CryptoAPI documentation indicates that the pdwDataLen parameter does not return the actual current size of data, but typically returns the maximum size of the buffer assigned. The prototype using a StringBuilder type automatically handles the string data length returned by the method StringBuilder.ToString(). However, for the prototype using a byte array argument, the returned string length must be determined manually by finding the terminal null character in the byte buffer, currbuff.IndexOf(nullchar) as the following commented code shows. The key container strings are stored in an ArrayList during the enumeration. The ArrayList is converted to a string array using ToArray() with a String type argument. This example also demonstrates how to retrieve Win32® errors and display error codes and messages.
If you explore cryptographic functionality and utilities extensively, you are sure to generate several key containers that you may not be aware of. As a student exercise, can you identify all the key containers enumerated on your system? Some key containers will be associated with commercial CA-issued certificates in your MY system certificate store. Other key containers will have no associated certificates. Bear in mind that some applications will automatically instantiate persistent keys/containers for their own use. As an example, my Windows 2000 system has several key containers associated with a Visual Studio.NET 2002 installation and are prefixed with "VS7". As another example, key containers generated with the default RSACryptoServiceProvider() constructor, and marked as PersistKeyInCsp=true will have key container names prefixed with "CLR". Interestingly, the .NET classes actually provide support for deletion of keys and key containers by setting RSACryptoServiceProvider.PersistKeyInCsp = false, followed by invoking the Clear() method on the RSA provider object. However, unless you are sure of what you are doing, it is not a good idea to delete keys whose purpose you don't clearly understand.
using System;
using System.Runtime.InteropServices;
using System.ComponentModel;
using System.Collections;
using System.Text;
public class Win32 {
[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern bool CryptAcquireContext(
ref IntPtr hProv,
string pszContainer,
string pszProvider,
uint dwProvType,
uint dwFlags) ;
[DllImport("advapi32.dll", SetLastError=true)]
public static extern bool CryptReleaseContext(
IntPtr hProv,
uint dwFlags) ;
/*BOOL WINAPI CryptGetProvParam(
HCRYPTPROV hProv,
DWORD dwParam,
BYTE* pbData,
DWORD* pdwDataLen,
DWORD dwFlags
);
*/
[DllImport("advapi32.dll", SetLastError=true)]
public static extern bool CryptGetProvParam(
IntPtr hProv,
uint dwParam,
[In, Out] byte[] pbData,
ref uint dwDataLen,
uint dwFlags) ;
[DllImport("advapi32.dll", SetLastError=true)]
public static extern bool CryptGetProvParam(
IntPtr hProv,
uint dwParam,
[MarshalAs(UnmanagedType.LPStr)] StringBuilder pbData,
ref uint dwDataLen,
uint dwFlags) ;
}
public class KeyContainers {
const String title = "KeyContainers";
const uint PROV_RSA_FULL = 0x00000001;
const uint CRYPT_VERIFYCONTEXT = 0xF0000000; //no private key access required
const uint CRYPT_FIRST = 0x00000001;
const uint PP_ENUMCONTAINERS = 0x00000002;
const uint CRYPT_MACHINE_KEYSET= 0x00000020;
public static void Main() {
string[] containernames = KeyContainers.GetContainerNames() ;
if(containernames == null) {
Console.WriteLine("Couldn't get containernames");
return;
}
Console.WriteLine("\n------- {0} key containers found: ---------- ", containernames.Length);
foreach(String container in containernames){
Console.WriteLine("{0} ", container) ;
}
Console.WriteLine("-------------------------------------------------");
}
private static string[] GetContainerNames(){
const int BUFFSIZE = 256;
ArrayList containernames = new ArrayList();
byte[] pbData = new byte[BUFFSIZE];
uint pcbData = BUFFSIZE;
String provider = null ; //can use null, for default provider
String container = null; //required for crypt_verifycontext
uint type = PROV_RSA_FULL ;
uint cspflags = CRYPT_VERIFYCONTEXT ; //no private key access required.
uint enumflags = PP_ENUMCONTAINERS ; //specify container enumeration functdionality
IntPtr hProv = IntPtr.Zero;
uint dwFlags = 0;
bool gotcsp = Win32.CryptAcquireContext(ref hProv, container, provider, type, cspflags);
if (!gotcsp){
showWin32Error(Marshal.GetLastWin32Error());
return null ;
}
/* ---------- Get KeyContainer Names ------------- */
ASCIIEncoding ascii = new ASCIIEncoding();
int nullindex = -1;
char nullchar = (char) 0 ;
int i=0;
String currbuff = "";
dwFlags=CRYPT_FIRST; //required initalization
StringBuilder sb = new StringBuilder(BUFFSIZE);
while (Win32.CryptGetProvParam(hProv, enumflags, sb, ref pcbData, dwFlags))
{
dwFlags=0; //required to continue entire enumeration
containernames.Add(sb.ToString());
}
//--------- An alternate way to enumerate, using overloaded prototype -----------------
//while (Win32.CryptGetProvParam(hProv, enumflags, pbData, ref pcbData, dwFlags))
//{
// dwFlags=0; //required to continue entire enumeration
// currbuff = ascii.GetString(pbData);
// nullindex = currbuff.IndexOf(nullchar) ; //get null end character
// containernames.Add(currbuff.Substring(0, nullindex));
// }
if (hProv != IntPtr.Zero)
Win32.CryptReleaseContext(hProv,0) ;
if(containernames.Count == 0)
return null ;
else
return (string[])containernames.ToArray(Type.GetType("System.String"));
}
private static void showWin32Error(int errorcode){
Win32Exception myEx=new Win32Exception(errorcode);
Console.WriteLine("Error code:\t 0x{0:X}", myEx.ErrorCode);
Console.WriteLine("Error message:\t " + myEx.Message);
}
}
Conclusion
I hope this article has provided some useful perspective and techniques for implementing cryptography functionality in .NET that complements the current impressive .NET Framework classes. As .NET-connected applications and services become more widely deployed through Web applications, services, and client applications, the use of cryptography to protect, verify, and authenticate information will become widespread. Developers have several approaches for designing and building .NET Framework 1.0/1.1 crypto-enabled applications, no matter how complex the cryptographic task at hand.
References
.NET Framework Cryptography Frequently Asked Questions
Adventures in Visual Basic .NET: Tales from the Crypto
Base64 Content-Transfer-Encoding
Building Secure ASP.NET Applications: Authentication, Authorization, and Secure Communcation
CAPICOM Reference, Platform SDK
COM Interop Part 1: C# Client Tutorial
Cryptographic Hash Algorithms Let You Detect Malicious Code in ASP.NET, MSDN Magazine, September 2002
Cryptographic Services, .NET Framework Developer’s Guide
The Cryptography API, or How to Keep a Secret
Encrypt/Decrypt Sample, GotDotNet
Geek Speak Decoded #9: Cryptography and Encryption
HOW TO: Deploy an Assembly to the Target Computer Global Assembly Cache, Microsoft PSS Knowledge Base Article
INFO: CryptAcquireContext() Use and Troubleshooting, Microsoft PSS Knowledge Base Article
N/Direct: The .NET Interoperability Resource Center
Object IDs Associated with Microsoft Cryptography, Microsoft PSS Knowledge Base Article
Protect Private Data with the Cryptography Namespaces of the .NET Framework, MSDN Magazine, June 2002
Public Key Cryptography Sample, .NET Framework SDK
Secure Coding Guidelines for the .NET Framework
A Simple Guide to Cryptography
System.Security.Cryptography Namespace, .NET Framework Class Library
System.Security.Cryptography.X509Certificates Namespace, .NET Framework Class Library
Using Cryptography, Microsoft Platform SDK
Windows 2000 Certificate Services and Public Key Infrastructure
Working with C#: Using Win32 and Other Libraries
Writing Secure Code, Michael Howard and David LeBlanc, 2nd Ed., Microsoft® Press, 2003
Acknowledgments
Many thanks to Ryan Hurst of the Windows Trusted Platform Infrastructure team for useful suggestions on content clarification.
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 designing puzzles and playing surf music on his '62 Jaguar.