Monday, November 03, 2003

ASP.NET and Client Certificate : Without .NET Enterprise Services


Microsoft has a fix to the System.DLL assembly that will allow an ASP.NET application to call a Web Service using the ASPNET account and SSL. You can find the notes to this fix at the following site http://support.microsoft.com/?id=817854

The architecture of the managed code is similar to the previous managed code version except there is no need to package the components in a COM+ Application. As a result all components can run in the same process space (and AppDomain). This will generally give better performance. However, you must find another way to keep your connections to the backend persisted (i.e., no Object Pooling). This is where you should consider using the ServicePoint and ServicePointManager classes.
Required Steps

1. Configure the ASPNET account to have access to the client certificate. You can accomplish this by using the winhttpcertcfg tool that comes with the WinHTTP SDK
a. winhttpcertcfg -g -c LOCAL_MACHINE\My -s MyCertificate -a ASPNET
b. Note: you can see who has access to a particular store by issuing the following command line command: winhttpcertcfg.exe -l -c LOCAL_MACHINE\My -s "mycertificate"

2. Apply the hot fix for the .NET Framework v1.0. You can find the relavant information at http://support.microsoft.com/?id=817854.

3. Export the client certificate to a file without the private key. You will not reference the client store directly; instead you will reference this file.

4. Implement the data access component using the WebResponse, WebRequest and optionally the ServicePoint and SerbvicePointManager classes.

5. Implement the ICertifciatePolicy interface.

6. Build the assembly and optionally signed the assembly with a strong name.

7. With the assembly now built and deployed to the Asp.NET application (i.e., virtual directory); you are now ready to use the certificate to communicate with the backend.

Sample Code


While this sample doesn't show it, you should build the data access component such that it is reusable (preferable via a configuration file) against any HTTP/HTTPS endpoints, etc. This sample also uses the ServicePoint and ServicePointManager classes to increase the number of persisted connections you can have against a single domain endpoint. The ServicePointManager class manages the connection for you and via the FindServicePoint method will return a connection if one already exist. If one does not exist, it will create a new connection.

The following files are associated with the data access component. The sample is meant to get the point across and as such all unnecessary code have been removed. In an enterprise version, you would have additional features such as: logging, tracing, configuration file, etc. (please email me to get a full sample).
You can test the sample by creating a client or ASP.NET web service to make a call to the MakeRequest method. Just pass the method the POST data (strData), the target URL (strURI e.g., https://:) and the method by which to sent the request (strMethod e.g., POST).

Post.cs file


using System;
using System.Net;
using System.Text;
using System.IO;
using System.Web;
using System.Security.Cryptography.X509Certificates;


namespace MyNamespace
{
public class HTTPDataAccess
{
public HTTPDataAccess( )
{
// Set a maximum of 20 connections to the host
ServicePointManager.DefaultConnectionLimit = 20;

// 1 minutes max ideal time
ServicePointManager.MaxServicePointIdleTime = 100000;
ServicePointManager.CertificatePolicy = new CertPolicy();
}

private string MakeRequest(string strData, string strURI, string strMethod)
{
// retrieve an existing connection to the specified URL (i.e., strURI) or create a new one
ServicePoint sp = ServicePointManager.FindServicePoint( strURI, null );

// create an instance of the httpWebRequest object
HttpWebRequest req = ( HttpWebRequest ) WebRequest.Create( sp.Address );

// add a client certificate to the http request object
req.ClientCertificates.Add( X509Certificate.CreateFromCertFile( @"D:\MyCertificates\dotnet.cer" ) );

// set the request method to POST, txml/xml with a 1 minute timeout
req.Method = "POST";
req.ContentType = "text/xml";
req.Timeout = 10000;
req.KeepAlive = true;

// if we have data to post, set the request stream object
if( strData != null )
{
byte[] SomeBytes = null;
SomeBytes = Encoding.UTF8.GetBytes( strData );
req.ContentLength = SomeBytes.Length;
Stream newStream = req.GetRequestStream( );
newStream.Write( SomeBytes, 0, SomeBytes.Length );
newStream.Close( );
}
else
{
req.ContentLength = 0;
}


WebResponse result = req.GetResponse( );
Stream ReceiveStream = result.GetResponseStream( );

Encoding encode = System.Text.Encoding.GetEncoding( "utf-8" );
StreamReader sr = new StreamReader( ReceiveStream, encode );

string strResponse = sr.ReadToEnd( );

sr.Close( );
result.Close( );
ReceiveStream.Close( );

return strResponse;
}
}
}

certpolicy.cs file


Same as .NET Enterprise Service Sample

ASP.NET and Client Certificate : .NET Enterprise Services


Currently the Microsoft .NET Framework has a bug that does not allow for a configured .NET component (i.e., a .NET component that is configured as an Enterprise Service Application a.k.a. COM+ application) to access the client certificate from the key store unless the COM+ Application first loads up the user profile. This is true even if you configure the identity of the COM+ Application to be the same as the identity of the client certificate was installed under. To work around this problem you must take a few more steps. This process is outlined below and is further explained in this Microsoft article http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnnetsec/html/SecNetHT13.asp.
Required Steps


1. Install the Client Certificate and note the id the certificate was installed under

2. Export the client certificate to a file without the private key. You will not reference the client store directly; rather you will reference this file

3. Implement the data access component using the WebResponse and WebRequest
a. Load the user profile the certificate is store under before accessing the certificate (see post.cs).
b. Implement the ICertifciatePolicy interface (see certpolicy.cs).
c. Extend you data access class with the ServiceComponent class (see post.cs)
i. Note: It is also advisable to create an interface to describe the public methods and implement the interface. This will allow you to see the public methods when viewing the configured component under the Component Service MMC snap-in.
d. Attribute the assembly to be a service component (assembly.cs …not shown).
e. Unload the user profile after the call has been made (see post.cs).

4. Build the assembly and signed the assembly with a strong name. This can be done via Visual Studio 2002/2003 or the Assembly Linker tool (al).

5. Register the assembly in the Component Service (RegSvcs) and copy to the GAC (gacutil).
a. regsvcs MyAssembly.DLL
b. gacutil /i MyAssembly.DLL

6. With the assembly registered in the GAC and configured as a .NET Enterprise Application (i.e., COM+ Application). Set the identity of the configured application to the user ID (i.e., principle) the certificate was installed with; you are now ready to use the certificate to communicate with the backend.

Sample Code


While this sample doesn’t show it, you should build the data access component such that it is reusable (preferable via a configuration file) against any HTTP/HTTPS endpoints, etc. You could also use the ServicePoint and ServicePointManager classes to increase the number of persisted connections you can have against a single domain endpoint. The ServicePointManager class manages the connection for you and via the FindServicePoint method will return a connection if one already exist. If one does not exist, it will create a new connection.
The following files are associated with the data access component. The sample is meant to get the point across and as such all unnecessary code have been removed. In an enterprise version, you would have additional features such as: logging, tracing, connection pooling, configuration file, etc. (please email me to get a full sample).
You can test the sample by creating a client or ASP.NET web service to make a call to the MakeRequest method. Just pass the method the POST data (strData), the target URL (strURI e.g., https://:) and the method by which to sent the request (strMethod e.g., POST).

Post.cs file


using System;
using System.Net;
using System.Text;
using System.IO;
using System.Web;
using System.Security.Principal;
using System.EnterpriseServices;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;

namespace NyNamespace
{
public class HTTPDataAccess : ServicedComponent
{
[DllImport("advapi32.dll", CharSet=CharSet.Auto,
SetLastError=true)]

private extern static bool DuplicateToken(IntPtr
ExistingTokenHandle,
int SECURITY_IMPERSONATION_LEVEL,
ref IntPtr DuplicateTokenHandle);

[DllImport("kernel32.dll", CharSet=CharSet.Auto)]

private extern static bool CloseHandle(IntPtr handle);


public HTTPDataAccess( )
{
ServicePointManager.CertificatePolicy = new CertPolicy();
}

private IntPtr DupeToken(IntPtr token, int Level)
{
IntPtr dupeTokenHandle = new IntPtr(0);
bool retVal = DuplicateToken(token, Level, ref dupeTokenHandle);
if (false == retVal)
{
return IntPtr.Zero;
}
return dupeTokenHandle;
}

public string PostRequest( string strData, string strURI )
{
try
{
bool retVal = false;

// Need to duplicate the token. LoadUserProfile needs a token
// with TOKEN_IMPERSONATE and TOKEN_DUPLICATE.

const int SecurityImpersonation = 2;
IntPtr dupeTokenHandle = DupeToken(WindowsIdentity.GetCurrent().Token,SecurityImpersonation);

if(IntPtr.Zero == dupeTokenHandle)
{
throw new Exception("Unable to duplicate token.");
}

// Load the profile.

ProfileManager.PROFILEINFO profile = new ProfileManager.PROFILEINFO();
profile.dwSize = 32;
profile.lpUserName = @"headlam6\dotnetacct";

retVal = ProfileManager.LoadUserProfile(dupeTokenHandle, ref profile);

if(false == retVal)
{
throw new Exception("Error loading user profile. " + Marshal.GetLastWin32Error());
}


// create an instance of the httpWebRequest object
HttpWebRequest req = ( HttpWebRequest ) WebRequest.Create( strURL );


// set the request method to POST, txml/xml with a 1 minute timeout
req.Method = "POST";
req.ContentType = "text/xml";
req.Timeout = 1000;


// create an .X509 certificate and associated it with the
// HttpWebRequest object
// the certificate info we exported are stored in the file D:\MyCertificates.cer in this
// example

X509Certificate x509 =
X509Certificate.CreateFromCertFile( @"D:\MyCertificates.cer" );
req.ClientCertificates.Add( x509 );

// if we have data to post, set the request stream object
if( strData != null )
{
byte[] SomeBytes = null;
SomeBytes = Encoding.UTF8.GetBytes( strData );
req.ContentLength = SomeBytes.Length;
Stream newStream = req.GetRequestStream( );
newStream.Write( SomeBytes, 0, SomeBytes.Length );
newStream.Close( );
}
else
{
req.ContentLength = 0;
}


// get the response form the back end
WebResponse result = req.GetResponse( );

Stream ReceiveStream = result.GetResponseStream( );

Encoding encode = System.Text.Encoding.GetEncoding( "utf-8" );
StreamReader sr = new StreamReader( ReceiveStream, encode );


// unload the user profile and clean up the handel
ProfileManager.UnloadUserProfile (WindowsIdentity.GetCurrent().Token, profile.hProfile);
CloseHandle(dupeTokenHandle);


// return the result to the ASP.NET code or the client
return sr.ReadToEnd( );

}
catch( WebException ex )
{
return ex.Message;
}
catch( Exception ex )
{
return ex.Message;
}
}
}
}

CertPolicy.cs


using System;
using System.Net;
using System.Security.Cryptography.X509Certificates;

namespace NyNamespace
{
//Implement the ICertificatePolicy interface
class CertPolicy: ICertificatePolicy
{
public bool CheckValidationResult(ServicePoint srvPoint,
X509Certificate certificate, WebRequest request, int certificateProblem)
{
// you can do your own certificate checking here
// you can get the error values from WinError.h,
// all the certificate errors start with Cert_


// we just return true so any certificate will work with this sample
return true;
}
}
}

ProfileManager.cs


using System;
using System.Runtime.InteropServices;


namespace NyNamespace
{
internal class ProfileManager
{
[DllImport("Userenv.dll", SetLastError=true,
CharSet=System.Runtime.InteropServices.CharSet.Auto)]

internal static extern bool LoadUserProfile(IntPtr hToken,
ref PROFILEINFO lpProfileInfo);

[DllImport("Userenv.dll", SetLastError=true,
CharSet=System.Runtime.InteropServices.CharSet.Auto)]

internal static extern bool UnloadUserProfile( IntPtr hToken, IntPtr hProfile);

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
public struct PROFILEINFO
{
public int dwSize;
public int dwFlags;
public String lpUserName;
public String lpProfilePath;
public String lpDefaultPath;
public String lpServerName;
public String lpPolicyPath;
public IntPtr hProfile;
}
}
}

Sunday, November 02, 2003

ASP.NET & Client Certificates


ASP.NET v1.0 applications have a problem delivering a client certificate to a secure endpoint. While there are ways around this problem using .NET Enterprise Services (a.k.a. COM+ Services) the performance with the .NET Enterprise Services solution is less than ideal and the implementation has less to be desired. Microsoft has since come out with a fix that allow ASP.NET v1.0 to deliver the certificate Ckeck out the article. The performance with this solution is much better and the design is very simple. I have not tested ASP.NET v1.1 to see if the original problem still exists… stay tuned.

PDC 2003


PDC 2003 was a blast. Microsoft for the first time publicly announced several products: Whidbey (the next version of Visual Studio .NET) and Longhorn (and http://www.longhornblogs.com/) (the next version of Windows). Within the Longhorn product there were four areas of interest: Avalon (a new UI interface), Indigo (a service oriented system for sending and processing messages), WinFS ( a new file system built on top of NTFS) and Yukon (the next version of SQL Server.

New C# Features.