Monday, November 03, 2003

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;
}
}
}

0 Comments:

Post a Comment

<< Home