Internet Explorer Client Certificate Management

The process for creating and installing a client certificate for Microsoft Internet Explorer is more complicated than with Netscape Navigator because Internet Explorer requires use of an ActiveX control to generate the certificate request, and also to install the certificate. This ActiveX control is called by a script embedded in the HTML page.

Before creating any Microsoft Internet Explorer Client Certificates it is necessary to create a Certificate Revocation List. Create an empty Certificate Revocation List (CRL) as follows:

cd ${SSLDIR}/crl
${SSLDIR}/bin/ssleay ca -gencrl -out crl.pem -config /opt/www/lib/ssleay.cnf
Using configuration from /opt/www/lib/ssleay.cnf
Enter PEM pass phrase: [enter CA key here]

A Microsoft Internet Explorer client certificate is created by:

  1. Using an HTML form to request a client certificate
  2. Processing the Request using a CGI script
  3. Using an HTML page to install the certificate

Although client certificates may be installed in Microsoft Internet Explorer, we are still investigating how to establish an SSL session when they are required. It may be the case that Internet Explorer does not support them. A new release of SSLeay is expected shortly, which may solve this problem.

HTML Form to Request Microsoft Internet Explorer Client Certificate

An HTML form is used to allow the user to fill in the fields for the distinguished name of the subject of the certificate. These fields include the commonName which should be unique. An example of the form is:

Sample Netscape Navigator User Certificate Form

This form also includes some hidden code which is executed on the browser when the form is submitted. This code, used to call an ActiveX control with the values of the form fields, may be written using JavaScript or VisualBasic. The sample scripts use JavaScript.

The ActiveX control generates a key pair, installs the private key in the browser, and returns the public key as part of a certificate request which is sent to the server in a hidden form field. The program calls the GenReqForm method of the certenr3 ActiveX control, passing it the distinguished name values from the form.

The Internet Explorer form source is:

<HTML><HEAD><TITLE>Client Certificate Request</TITLE></HEAD><BODY>

<!-- Use the Microsoft ActiveX control to generate the certificate -->
<OBJECT CLASSID="clsid:33BEC9E0-F78F-11cf-B782-00C04FD7BF43"
        CODEBASE=certenr3.dll
	   ID=certHelper>
</OBJECT>

<!-- JavaScript or Visual Basic will work. -->
<SCRIPT LANGUAGE="JavaScript">
<!---

// this is from JavaScript: The Definitive Guide, since
// Microsoft implementation of Math.random() is broken
//
function random() {
    random.seed = (random.seed*random.a + random.c) % random.m;
    return random.seed/random.m;
}
random.m = 714025; random.a = 4096; random.c = 150889;
random.seed = (new Date()).getTime()%random.m;

function GenReq ()
{
    var sessionId		= "a_unique_session_id";
    var reqHardware     	= 0;
    var szName          	= "";
    var szPurpose       	= "ClientAuth";
    var doAcceptanceUINow   	= 0;
    var doAcceptanceUILater	= 0;
    var doOnline        	= 1;
    var keySpec = 1;

    szName = "";
    
    if (document.GenReqForm.commonName.value == "")
    {
	alert("No Common Name");
	return false;
    } 
    else
     szName = "CN=" + document.GenReqForm.commonName.value;

    if (document.GenReqForm.countryName.value == "")
    {
	alert("No Country");
	return false;
    }
    else
	szName = szName + "; C=" + document.GenReqForm.countryName.value;

    if (document.GenReqForm.stateOrProvinceName.value == "")
    {
	alert("No State or Province");
	return false;
    }
    else
	szName = szName + "; S=" + document.GenReqForm.stateOrProvinceName.value;

    if (document.GenReqForm.localityName.value == "")
    {
	alert("No City");
	return false;
    }
    else
	szName = szName + "; L=" + document.GenReqForm.localityName.value;

    if (document.GenReqForm.organizationName.value == "")
    {
	alert("No Organization");
	return false;
    }
    else
	szName = szName + "; O=" + document.GenReqForm.organizationName.value;

    if (document.GenReqForm.organizationalUnitName.value == "")
    {
	alert("No Organizational Unit");
	return false;
    }
    else
	szName = szName + "; OU=" + document.GenReqForm.organizationalUnitName.value;

    /* make session id unique */
    sessionId = "xx" + Math.round(random() * 1000); 

    sz10 = certHelper.GenerateKeyPair(sessionId, reqHardware, szName,
                                      0, szPurpose, doAcceptanceUINow, 
                                      doOnline, keySpec, "", "", 1);

    /*
     *
     * The condition sz10 being empty occurs on any condition in which the
     * credential was not successfully generated. In particular, it occurs
     * when the operation was cancelled by the user, as well as additional
     * errors. A cancel is distinguished from other unsuccessful
	* generations by an empty sz10 and an error value of zero.
     *
     */

    if (sz10 != "")
    {
	document.GenReqForm.reqEntry.value = sz10;
     document.GenReqForm.sessionId.value = sessionId;
    } else {
	alert("Key Pair Generation failed");
	return false;
    }
}

//--->
</SCRIPT>

<CENTER><H3>Generate key pair and client certificate request</H3></CENTER>

<FORM METHOD=POST ACTION="http://example.opengroup.org/cgi-bin/ms_key.pl"
     NAME="GenReqForm"  onSubmit="GenReq()">
<TABLE>
<TR><TD>Common Name:</TD><TD>
<INPUT TYPE=TEXT NAME="commonName" VALUE="Client Certificate" SIZE=64>

</TD></TR><TR><TD>Country:</TD><TD>
<INPUT TYPE=TEXT NAME="countryName"  VALUE="US" SIZE=2>

</TD></TR><TR><TD>State or Province:</TD><TD>
<INPUT TYPE=TEXT NAME="stateOrProvinceName" VALUE="MA">

</TD></TR><TR><TD>City:</TD><TD>
<INPUT TYPE=TEXT NAME="localityName" VALUE="Cambridge">

</TD></TR><TR><TD>Organization:</TD><TD>
<INPUT TYPE=TEXT NAME="organizationName" VALUE="The Open Group">

</TD></TR><TR><TD>Organizational Unit:</TD><TD>
<INPUT TYPE=TEXT NAME="organizationalUnitName" VALUE="Research Institute">

</TD></TR></TABLE>

<INPUT TYPE=HIDDEN	NAME="sessionId">
<INPUT TYPE=HIDDEN	NAME="reqEntry">

<INPUT TYPE="SUBMIT" name="SUBMIT">
</FORM>

</BODY></HTML>

CGI Script for Processing Internet Explorer Client Certificate Requests

A CGI script is used to process the form data passed by the HTML form. It is more complicated than the Netscape Navigator CGI script because it must create an HTML page containing JavaScript which calls an ActiveX control in order to install the client certificate.

The CGI script does the following:

  1. Validates and reformats the certificate request passed in the hidden field.
  2. Calls SSLeay "ca" command to create a certificate from the request.
  3. Combines the certificate with a certificate revocation list to create a PKCS#7 certificate.
  4. Dynamically generates an HTML form containing the certificate.
  5. Sends the HTML form to browser to install the certificate.
The script takes the certificate request generated by Internet Explorer from a hidden form field, and reformats it so that each line in the request is 72 characters long. It then passes this certificate request to the SSLeay "ca" command to generate a certificate, as follows:
$SSLDIR/bin/ca -in $req_file -out $result_file -days 360 -policy policy_match \
	-config /opt/www/lib/ssleay.cnf  -key $CAPASS 2>errs
This example shows the command after some of the Perl processing to create the command has been performed. The $req_file variable contains the name of a unique file in the certs directory used to contain the reformatted certificate request (the file is useful for debugging). The $result_file variable contains the name of a unique file in the certs directory used to contain the certificate. The $CAPASS Perl variable contains the CA key.

Once the certificate has been successfully generated, the SSLeay "crl2pkcs7" utility is used to combine the certificate with the SSLeay certificate revocation list (CRL) to create a PKCS#7 certificate. This is done using the crl2pkcs7 command as follows:

$SSLDIR/bin/crl2pkcs7 -certfile $result_file -in $CRL -out $pkcs7_file 2>errs
This example shows the command after some of the Perl processing to create the command has been performed. The $result_file variable contains the name of a unique file in the certs directory used to contain the certificate. The $pkcs7_file Perl variable contains the name of a unique file in the certs directory used to contain the result PKCS#7 certificate. The $CRL Perl variable contains $SSLDIR/crl/crl.pem, the file containing the Certificate Authority certificate revocation list.

A certificate revocation list may be created using SSLeay as follows:

$SSLDIR/bin/ca -gencrl -config /opt/www/lib/ssleay.cnf -out $SSLDIR/crl/crl.pem

Once the PKCS#7 certificate has been successfully generated, an HTML page is dynamically generated. This page contains JavaScript code which calls an ActiveX control to install the certificate in the browser. The page in this example is designed to automatically load the certificate once the page has loaded into the browser, by using the JavaScript "onLoad" command. In a production system such automatic installation may not be desired.

The CGI script is as follows:

#!/usr/local/bin/perl 

require 5.003;
use strict;
use CGI;

use File::CounterFile;         	# module to maintain certificate request counter

my $SSLDIR  = '/opt/dev/ssl';
my $CA = "$SSLDIR/bin/ca";
my $CRL2PKCS7 = "$SSLDIR/bin/crl2pkcs7";
my $CONFIG =  "/opt/www/lib/ssleay.cnf";
my $CRL = "$SSLDIR/crl/crl.pem";
my $CAPASS = "caKEY2";

my $doc_dir = $ENV{'DOCUMENT_ROOT'};	# apache specific location for storage
unless($doc_dir) {
    print "<HTML><HEAD><TITLE>Failure</TITLE></HEAD><BODY>DOCUMENT_ROOT not defined</BODY></HTML>";
    exit(0);
}
my $base_dir = $doc_dir;
$base_dir =~ s/\/htdocs//;

my $query = new CGI;

my $req = $query->param('reqEntry');

unless($req) { fail("No Certificate Request Provided"); }

my $counter = new File::CounterFile("$base_dir/.counter", 1);
unless($counter) { fail("Count not create counter: $!"); }
my $count = $counter->inc();

my $certs_dir = "$base_dir/certs"; 
my $req_file = "$certs_dir/cert$count.req";
my $result_file = "$certs_dir/cert$count.result";
my $key_file = "$certs_dir/$count.key";
my $debug_file = "$certs_dir/$count.debug";
my $pkcs7_file = "$certs_dir/cert$count.pkcs";

#process request
$req =~ tr/
//d;
$req =~ tr/\n//d;

# save the certificate request to a file, as received
open(REQ, ">$req_file") or fail("Could no save certificate request to file");

print REQ "-----BEGIN CERTIFICATE REQUEST-----\n";
my $result = 1;
while($result) {
    $result = substr($req, 0, 72);
    if($result) {
	print REQ "$result\n";
	$req = substr($req, 72);
    }
}
print REQ "-----END CERTIFICATE REQUEST-----\n";
close(REQ);

unless(-e $CA) { fail("$CA command missing"); }

my $cmd = "$CA -config $CONFIG -in $req_file -out $result_file -days 360 -policy policy_match";
my $rc =  system("$cmd -key $CAPASS 2>errs <<END\ny\ny\nEND");

my $session = $query->param('sessionId');
my $cn = $query->param('commonName');

if($rc != 0) { fail("Certification Request Failed</h2>$cmd<P>rc = $rc<P>\
                       sessionID = $session<BR>req = $req<BR>", "errs"); }

my $cmd = "$CRL2PKCS7 -certfile $result_file -in $CRL -out $pkcs7_file";
my $rc =  system("$cmd 2>errs");

open(CERT, "<$pkcs7_file") or fail("Could not open $pkcs7_file<P>$!");
    
my $certificate = "";
my $started = 0;
while(<CERT>) {
    if(/BEGIN PKCS7/) {
	$started = 1;
	next;
    }
    if(/END PKCS7/) {
	last;
    }

    if($started) { 
	chomp;
	$certificate .= "$_"; 
    }
}
close(CERT);

open(MSG, ">msg") or fail("Could not generate message");

print MSG <<_END_TEXT_;
<HTML><HEAD><TITLE>Finish Client Certificate Installation</TITLE>

<!-- Use the Microsoft ActiveX control to install the certificate -->
<OBJECT  CLASSID="clsid:33BEC9E0-F78F-11cf-B782-00C04FD7BF43"
         CODE=certenr3.dll
         ID=certHelper>
</OBJECT>

<SCRIPT LANGUAGE="JavaScript">
<!--
function InstallCert (subject, sessionId, cert)
{
    if( sessionId == "") {
	alert("No Session id");
	return;
    }

    if(cert == "") {
	alert("No Certificate");
	return;
    }

    var doAcceptanceUILater = 0;

    result  = certHelper.AcceptCredentials(sessionId, cert, 0,
                                           doAcceptanceUILater);


    if(result == "") {
	var msg = "Attempt to install " + subject + " client certificate failed";
	alert(msg);
	return false;
    } else {
	var msg = subject + " client certificate installed";
	alert(msg);
    }
}
-->
</SCRIPT>
</HEAD>

<BODY onLoad="InstallCert('$cn', '$session', '$certificate');">
Installing client certificate for $cn<BR>
session: $session<BR>
</BODY>
</HTML>

_END_TEXT_

close(MSG);

open(RD, "<msg") or fail("Could not open msg file");
my $msg = join '', <RD>;
close(RD);

my $len = length($msg);

print "Content-Type: text/html\n";
print "Content-Length: $len\n\n";
print $msg;

exit(0);

sub fail {
    my($msg, $errs) = @_;

    print $query->header;
    print $query->start_html(-title => "Certificate Request Failure"); 
    print "<H2>Certificate request failed</H2>$msg<P>";
    if($errs) {
	if(open(ERR, "<errs")) {
	    while(<ERR>) {
		print "$_<BR>";
	    }
	    close ERR;
	}
    }
    print $query->dump();
    print $query->end_html();
    exit(0);
}

1;

Cookbook