Client PKI (x509) Certificates with Mac OS X Server


Integrating Mac OS X Server into a PKI sounds scary and hard, but it really isn't. This article will teach you how to set up Mac OS X Server to authenticate PKI (or x509) client certificates for static web pages, WebObjects apps, Perl CGIs and PHP.

"This new web site needs to authenticate with PKI certificates. Can your Mac server do that? If not..." Not the kind of thing server admin who is trying to hold on to his Mac server wants to hear. Yet, that is exactly what I heard a few years ago. After some hard work, I was able to answer yes. Now I know how to do it, and I'm going to share that knowledge with you.

Some ground rules, first. My experience of certificates is within a Public Key Infrastructure. That is, there is a root certificate that my clients have gotten their certificates from, and I got my server certificate from the same chain. It wasn't necessarily the same source, but the two chains meet up at some point. I haven't worked with client certificates in the real world, where the certificates come problem multiple roots. I have read the mod_ssl web site about this situation, and will discuss it when we get to that point, but I have no direct experience with it myself.

I'm also not going to get into the nuts and bolts of how clients and servers go through the dance of exchanging certificates. We are just going to go through the steps of setting up your Mac OS X 10.4 Server to allow your clients to authenticate with PKI (x509) certificates. This can be done on previous versions Mac OS X Server, but it requires a little more work an a trip to the command line. If you are in that environment, drop me an email, and I'll be happy to give you some pointers.

To start with, you will need to set up your web site to work with SSL. Let's begin by creating the certificate request. Open up Server Admin and connect to your server. Select the server, then click the Settings button at the bottom, and finally click on the Certificate tab:

Click the Plus sign. You will see a form to fill out:

The Common Name should be set to the DNS name of your site. Check with your certificate authority about how they want the rest of the fields filled out. My company was particular about the organization, less so about the Organization Unit, and the locality was the actual city and state we're in. In the post they have been more particular about the Organization Unit, so make sure you find out what yours wants. Type a pretty robust passphrase so your certificate won't be comprised. And remember the passphrase. You'll need it later.

When you get everything filled out, click the Save button. If you don't, you won't be able to click the "Request SIgned Certificate From CA..." button, and that is the point of this exercise, after all. After you Click Save, click "Request SIgned Certificate From CA..." and you will see a sheet:

Follow the instructions. If you need to put the CSR into some other mechanism, you can drag the certificate icon to the desktop, and you will get a text clipping including the CSR. However you send the CSR in, you will eventually get back a signed certificate, and hopefully a CA Chain file, which will give you the certificates of the signer, and its signers all the way back to a root. When you do, you will need to return to the Server Admin, and the certificates Setting, and edit the self signed certificate you just created. Click the "Add Signed Certificate" button. You will see:


Follow the instructions. My company's CA returned me the signed certificate in a plcs7 format. I had to take a trip to the command line, where openssl pkcs7 -in file.pem -print_certs printed out what I wanted. Actually, it printed the whole chain, so by executing openssl pkcs7 -in file.pem -print_certs -out certs.pem, I was able to create a chain file as well. This isn't needed for basic SSL operation, but it is necessary for authenticating client certificates.

Now we're ready to set up the web site. Start by clicking Web in the list of services for the server in Server Admin, and click the Sites tab:

You probably have a site at port 80 that you would just as soon keep running there. Click the + sign at the bottom to add a site, and click the pencil to edit it. You should set up the general settings for your new site. Then click the Security tab:



Check the "Enable Secure Sockets Layer (SSL)" check box. If you didn't enter 443 as the port under the General settings, Server Admin will inform you that it is switching the port to 443 for you. Now select your site certificate from the Certificate popup.

When you click Save, we have done as much as we can in Server Admin. We now have to start editing conf files. I am going to give you the basic how-to, but if you want to understand SSL, I would recommend that you start in the same place I did, the mod_ssl website .

I think that editing conf files is best done in the Terminal. To start, we will need to navigate to the place where the site configuration files are. cd to /etc/httpd/sites/. You will see there a conf file for each site you have on your server (a site is determined by a unique combination of IP number, port number and dns name. If you cat (that is, print out) the conf file for the site you just created, you will see something like:

## Default Virtual Host Configuration

<VirtualHost *:443>
ServerName server.quandir.com
ServerAdmin admin@quandir.com
DocumentRoot "/Library/WebServer/Documents"
DirectoryIndex index.html index.php
CustomLog "/var/log/httpd/access_log" "%h %l %u %t \"%r\" %>s %b"
ErrorLog "/var/log/httpd/error_log"
ErrorDocument 404 /error.html
<IfModule mod_ssl.c>
SSLEngine On
SSLLog "/var/log/httpd/ssl_engine_log"
SSLCertificateFile "/etc/certificates/server.quandir.com.crt"
SSLCertificateKeyFile "/etc/certificates/server.quandir.com.key"
SSLCipherSuite "ALL:!ADH:RC4+RSA:+HIGH:+MEDIUM:+LOW:!SSLv2:+EXP:+eNULL"
</IfModule>
<IfModule mod_dav.c>
DAVLockDB "/var/run/davlocks/.davlock100"
DAVMinTimeout 600
</IfModule>
<Directory "/Library/WebServer/Documents">
Options All -Indexes -ExecCGI -Includes +MultiViews
AllowOverride None
<IfModule mod_dav.c>
DAV Off
</IfModule>
</Directory>
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_METHOD} ^TRACE
RewriteRule .* - [F]
</IfModule>
<IfModule mod_alias.c>
</IfModule>
LogLevel warn
</VirtualHost>

This is a good start, and would allow us to serve web pages over SSL. But there are some modifications we need to make if we want to authenticate client certificates. The modifications are mostly in the <IfModule mod_ssl.c> block.

If we are inside a Public Key Infrastructure, where the server certificate and the client certificates all come from the same root, we need to get our certificate chain in there. It is probably a good idea to copy the certificate chain file into the /etc/certificates/ directory, since that is where the other certificates are. Then we can add a SSLCertificateChainFile directive that points to that file. Then we need to add a SSLCACertificateFile directive that points to the same file (I know it looks like the same directive, but it's not. See the CA after the SSL?).

Of course, this doesn't make a lot of sense if our root is not the source of the client certificates, especially if the clients are from multiple roots. In this case, we still want to give our own chain with the SSLCertificateChainFile directive. But we have a choice for the CAs of the clients. We can either put their root certificates into a directory and point to it with the SSLCACertificatePath directive. Or you can assemble the root certificates into an all-in-one file and use the SSLCACertificateFile directive. Again, I haven't done this myself. I commend you to the mod_ssl web site for more details.

You may not need to, but I found I had to modify the SSLCipherSuite directive. Unless it simply says "ALL", the browsers I have used complain that the server is allowing less than 128 bit encryption.

The directive that actually makes it possible to authenticate with certificates is SSLVerifyClient require. Without this directive, the browser wouldn't send the client certificate to the server. Actually, this is not quite true. There is another value for this directive that was designed to get the browser to send a certificate if it had one, and that is "optional." In practice, however, that value is almost never used because one browser misinterprets it. OK, let me call a spade a spade. The "optional" value was designed to ask for a certificate, and if one wasn't available to let the server decide what to do. The Microsoft Internet Explorer team, in their infinite arrogance, decided that the optional referred to the browser, and that if it was optional, they just wouldn't return it, even if the browser had one. As you can tell, this has caused me a fair amount of trouble, and some quite inelegant workarounds.

Next, we need to tell the server how deep to look in the signer chain for a match. I check deeper than I need to, but it doesn't seem to matter. The SSLVerifyDepth 10 directive takes care of that.

That's all the <IfModule mod_ssl.c> block changes needed if you just want to authenticate for static pages or WebObjects. If you need Perl or PHP, you will need one more directive, SSLOptions +StdEnvVars. This tells mod_ssl to hand the environment variables (including the certificate information) to cgi or PHP. The mod_ssl website says that this will slow down operation, but if you need it, you need it.

So, the complete <IfModule mod_ssl.c> block would be:

<IfModule mod_ssl.c>
SSLEngine On
SSLLog "/var/log/httpd/ssl_engine_log"
SSLCertificateChainFile "/etc/certificates/quandir_cert_ca.crt"
SSLCertificateFile "/etc/certificates/server.quandir.com.crt"
SSLCertificateKeyFile "/etc/certificates/server.quandir.com.key"
SSLCACertificateFile "/etc/certificates/quandir_cert_ca.crt"
SSLCipherSuite "ALL"
SSLVerifyClient require
SSLVerifyDepth 10
SSLOptions +StdEnvVars
</IfModule>

Just a little more and we'll be done with the conf file. What we are about to add is actually all we need to do to authenticate with client certificates for static web pages. The Directory directive sets the realm for access. You can use the SSLRequire directive inside the Directory directive in order to specify the conditions for access. Once again, I commend the mod_ssl web site to you for details on the kinds of expressions you can you, but as an example, you can check for the presence of one of a set of email addresses in the certificate subject:

<Directory "/Volumes/Storage/SecureWeb/Documents/myOrg/allhands">
SSLRequire %{SSL_CLIENT_S_DN_Email} in {"Charley.McCarthy@dummy.com","Mortimer.Snerd@dummy.com","Knucklehead.Smith@dummy.com"}
</Directory>

When you restart the webserver, you are serving static web pages and authenticating with client certificates.

Now for the dynamic stuff. The WebObjects adapter that comes with Mac OS X 10.4 Server is set to relay the SSL headers to your WebObjects application. Again, it is possible to do this for previous versions of Server, you will just need to uncomment a line in the adaptor sample code, recompile it and put it in the right place. Drop me an email if you need help. You need to have this in your Session code, somewhere that will be checked before sending back a page:

String str = this.context().request().headerForKey("ssl_client_cert_cn");

Then you need to process str to parse out the part of the certificate subject that you need, e.g., the email address. And of course, you will want to test it, probably checking it against a database of users.

The process is pretty much the same for Perl. As I mentioned above, for Perl and PHP, we need to set the directive in the conf file to hand over the environment variables. Once we do that, our Perl CGI can look something like this

#!/usr/bin/perl -w

use DBI; # You'll want to use something to talk to a database. I used DBI, you can do whatever you like.
use strict;

my $email = $ENV{"SSL_CLIENT_S_DN_Email"}; #You don't have to test against email address. See the mod_ssl website for other choices.
$email =~ tr/A-Z/a-z/; #this lowercases the email address. Its utility depends on the way you store the email addresses to compare against.

#That's about it. Pass this off in a database query or test it in any way you like.
#If the test is successful, proceed with what your CGI is supposed to do. If not, you can tell the user why not and end.

The technique for PHP is very similar, again checking the environment variables. This code in a php file:

<?=$_SERVER["SSL_CLIENT_S_DN_Email"]?>

should print out the email of the certificate owner. Caveat: I don't really know PHP. This snippet courtesy of my friend, Chris Newton, who does know php.

There, that wasn't hard, was it?

Posted: Mon - March 20, 2006 at 05:54 PM          


©