While writing a game that I'm in the process of creating, I decided that I'd make it as "complete" as possible.  As I thought of things, I bolted them onto the code, things such as achievements, internationalisation, and copy protection - all done before by other libraries, games, distribution methods etc. but it provided nice little sidelines, away from the bulk of code that I could grow weary of and onto more interesting code in other areas.  It's also good exercise to expose yourself to exactly what those things involve so you can get an idea of how to cope with them in the rest of your code.

While thinking about copy-protection, I decided the easiest way was probably to use X509 certificates - they are standardised, easily creatable and pretty secure (in that making "fake" ones that we can't detect is almost impossible).  For sure, I've never considered copy protection systems to be in any way secure, no matter how much time, effort or money you spend on them - you can't make a secure copy protection system, as countless commercial ventures have eventually gone on to prove - so my thought was merely to use something that left the only avenue to circumventing it to be modifying game code.  This is enough to deter casual copying without actually being too onerous.  If someone is determined enough to enter a debugger and start modifying your code, there's virtually nothing you can do about it except dirty tricks and things that might break legitimate users and uses of the program.

Specific copy-paths I wanted to deter were:

  • "False" activation:

    I once played a shareware game that required a ".reg" file from the author in order to activate the full version.  I discovered that merely copying any savegame from the same game to a file named ".reg" was enough to verify against the activation code and unlock the full version.  Whether this was intentional, or just blind luck that a save game was a valid registration file, I don't know (other files didn't seem to work - I found out the trick because I was trying to see if *ANY* file called "gamename.reg" was enough to make it think it was registered, happened to pick a savegame and was shocked when it worked.  It wasn't until later that I discovered that only savegames seemed to work!).  The fact that I had bought the game anyway was neither here nor there - I was just interested in how they'd made the activation system.

    Combatting this should mean that it's difficult for someone with only the "unregistered" game to register it without knowledge of what a registered copy contains, or to fake such a registration easily even when they do know what a registered copy contains.

  • Placing your own registered copy on the Internet:

    The installation should be personal enough to identify anyone who did this, and potentially disable their registration.  Or at least, make those who would otherwise casually copy something think twice because it could potentially be traced back to them.

The plan was to use signed X509 certificates, the same as those used for certifying SSL websites but without needing any sort of Internet connection or key infrastructure.

The steps are thus:

  1. Generate a CA (certification authority) X509 certificate and key.  Keep the key utterly private, but include the certificate with the game somehow.
  2. For each legitimate user, generate an X509 client certificate with their details inside, that is signed by the CA certificate.  Distribute this certificate to that individual user.  Only legitimate users would have a correctly signed certificate from the correct CA (i.e. me).
  3. Have the game code check the supplied user's certificate against the included CA certificate and fail if they are incorrectly signed, expired, etc.  Possibly display the users details from the certificate in-game somehow.
  4. (Optional) Go the whole hog and throw in certificate revocations, etc., for users that might distribute their certificates so other people can piggy-back on their registered copies.

Of course, there are a million and one attacks on even this system, from replacing the included CA certificate with your own self-signed one, to patching the SSL library calls to return valid values for invalid certificates, etc. but that's outside the scope of *any* copy-protection system anyway.  There are various safeguards against such things (checksum the certificate at runtime, checksum the code at runtime, buy a code-signing licence, etc.) but circumventing any of them would involve the same amount of code-inspection and patching as anything else.  But a casual copier won't be able to sign a valid client certificate in any name, or copy their one without their name getting out.  The point of this system is to make it so that it's *easier* to dig into the code and change checks that it would be to try to otherwise circumvent it - once someone is forced into changing the code itself, there's nothing more you can do anyway.

OpenSSL is the most well-known and widely-used (and most generally considered secure) of the SSL libraries available so I thought it would make an excellent candidate being so widely-distributed, and under a quite permissive licence.   It also provides some pre-compiled utilities for key-generation to save an awful lot of hassle when it comes to distributing keys to users.

Steps 1 and 2 are easy, it turns out, and are documented in many tutorials for everything from Apache SSL to OpenSSH, but usually using only the OpenSSL pre-built tools rather than the library or code itself.  I wasn't too interested in being able to do this programmatically, so for testing I used the "easy-rsa" utilities that are available with a copy of OpenVPN that I run.  They are basically DOS batch files that run a Windows OpenSSL executable to do the job of creating and signing certificates.  After initial setup and configuration, you can run "build-ca" to create a CA certificate, "build-key" to create a client certificate and be prompted for any information that you'd need to change each time.  There is also a revocation tool, for those who might want to produce a CRL (revocation list) to stop known-wild keys from being accepted (though this would require more than I describe here - for a start, an internet connection and clients checking it on each execution!)

It took a few dry runs and a few typos and other mistakes but eventually I ended up with:

    • CA.crt - a self-signed "certificate authority" certificate with certificate-signing capabilities.
    • client.crt - a client certificate with user details included, signed by the CA certificate.

This process can be automated in a number of ways (the "easy-rsa" utilities are nothing more than DOS batch files, for instance) for the developer and, because you don't have to distribute this key-signing infrastructure, you can use just about anything to do the job (it doesn't even need to be OpenSSL in theory - just any conforming X509 certificate generator).

Step 3 turned out to be a little more complex.  At first, I just assumed that OpenSSL would have an API that a human could read.  Sadly not.  So there must be some human-readable documentation on it somewhere, then.  Apparently, no.   Okay, so there HAS to be a myriad documents examples and HOWTO's from other people.  You would think.

In fact, I've never seen anything quite so undocumented as the OpenSSL library.  And I've never read quite so many comments from people who all say the same thing.  Sure, if I struggled through it and spent enough time on it, I'd get to know it inside out but that's not the point.  There just isn't any way to ease you into it, there's no hint of what's a secure operation and what's not.  There is no simple "I have a certificate and a CA and I want to check one is signed by the other" function or example of actually doing that programmatically (there are lots of examples in the OpenSSL source, but none of them are exactly readable and in most cases there is no commenting of what's happening at all).  The closest you get is random newsgroup postings and some horribly obscure and undocumented examples in the library source itself.  If the lack of documentation is supposed to discourage people from tinkering in things they don't understand, it actually does the opposite because those who HAVE to tinker in it can never be certain that they've understood it properly.

And that's assuming you can find anything on-line that actually discusses use of the library itself rather than just the pre-built tools.  It appears that OpenSSL's approach to documentation of the actual library is "Figure it out yourself", which seems very odd for such a popular and open library.

Which brings me to the point of this article.  I figured out what I needed, after much frustration, and thought that other people might find it helpful too.  Although I will follow in the footsteps of those most annoying of example-creators by omitting certain boilerplate code, I think most of this is easily understandable to anyone with a basic grounding in C or similar languages.  This file is partly based on a heavily-cut-down verify.c from OpenSSL, so consider it to be under the following licence:  http://www.openssl.org/source/license.html

The headers required are pretty standard:

#include <openssl/bio.h>
#include <openssl/pem.h>
#include <openssl/x509.h>
#include <openssl/x509v3.h>

You'll need two .crt files as described above (the CA and the client).  If you want to check you have an X509 file in Windows, you should be able to rename them to .crt and double-click on them to view the certificate details.   We start with a simple function that we can call to verify a client certificate (provided as a X509 file) against a CA certificate (also provided as an X509 file).  [Note: OpenSSL does let you use BIO_ functions to load the client certificate from memory directly.  But I can't seem to see an easy way to provide for CA certificates in the same way.]  

int verify_x509_certificate(const char* certfile, const char* CAfile);

// Verify the player's certificate is correctly signed.
int verification_result = verify_x509_certificate("certificates/client.crt", "certificates/ca.crt");

This function will return 0 for an incomplete verification (e.g. the certificate has expired, or other problems, but it was validly signed at the time), 1 for a complete verification, and anything else returned (usually -1) is a failure.

int verify_x509_certificate(const char* certfile, const char* CAfile)
{
int ret=0;
X509_STORE *cert_ctx=NULL;
X509_LOOKUP *lookup=NULL;
time_t check_time;

// Create a new X509 certificate store.
cert_ctx = X509_STORE_new();
if(cert_ctx == NULL)
{
printf("Could not create new X509 store\n");
return(-1);
};

// Load all available digests and ciphers
OpenSSL_add_all_algorithms();

// Create a list of locations that CA certs etc. can be found in.
lookup = X509_STORE_add_lookup(cert_ctx, X509_LOOKUP_file());
if(lookup == NULL)
{
printf("Could not add lookup to X509 store\n");
return(-1);
};

// Add the CA file into the lookup locations.
// We add it as a particular file, but it's possible to add directories of "known-good" CA's here too.
if(!X509_LOOKUP_load_file(lookup, CAfile, X509_FILETYPE_PEM))
{
printf("Could not load CA certificate into X509 store\n");
return(-1);
};

// Add the lookup locations to the store settings
lookup = X509_STORE_add_lookup(cert_ctx, X509_LOOKUP_hash_dir());
if(lookup == NULL)
{
printf("Could not add lookup to X509 store\n");
return(-1);
};

// Be sure to search in the lookup when we're looking for certificates files.
X509_LOOKUP_add_dir(lookup, NULL, X509_FILETYPE_DEFAULT);
// We now need to generate a time to compare against for certificates. By default, it would use the system time but we might want to, say,
// compare against another time (e.g. if we wanted "permanent" licenses that don't expire, we could compare against the executable's file time
// or some other static time value).

// Get the current time
time(&check_time);

// Check the user certificate against the X509 store, and pretend that check_time is the current time.
ret = check_x509_certificate(cert_ctx, certfile, check_time);

// Free the store
if(cert_ctx != NULL)
X509_STORE_free(cert_ctx);

return ret;
}

As you can see, this is mainly just preparation and the bulk of the work is done in a function check_x509_certificate.  This function also has the same return codes as the above.

int check_x509_certificate(X509_STORE *ctx, const char *file, time_t check_time)
{
X509 *x = NULL;
int i = 0, ret = 0;
X509_STORE_CTX *csc;

// Load the user's certificate from the given filename
x = load_x509_certificate(file);
if(x == NULL)
{
printf("Could not load certificate %s\n", file);
return(-1);
}

// Check to see if the certificate is that of a client, or a CA.
if(is_CA_certificate(x))
{
printf("Client certificate is actually a CA certificate - failing\n");
return(-1);
};

// Create a new CTX store.
csc = X509_STORE_CTX_new();
if(csc == NULL)
{
printf("Could not create new CTX store\n");
return(-1);
};

// Set various flags that affect the verification.
// X509_V_FLAG_USE_CHECK_TIME - make the given time be the one that we check certificates against.
X509_STORE_set_flags(ctx, X509_V_FLAG_USE_CHECK_TIME);

// Initialise the store with the given (client) certificate
if(!X509_STORE_CTX_init(csc, ctx, x, 0))
{
printf("Could not initialise new CTX store\n");
return(-1);
}

// Set the time that the store will use to check against the certificate.
X509_STORE_CTX_set_time(csc, 0, check_time);

// Try to verify the certificate against the loaded CA's.
i = X509_verify_cert(csc);

int error_return_value = X509_STORE_CTX_get_error(csc);
switch(error_return_value)
{
case X509_V_OK:
// TODO: (Use X509_get_subject_name instead? - OpenSSL need to fecking document their code better!)
// Example name:
// /C=GB/ST=Nowheretown/L=Bloggs/O=Org/OU=Org Unit/CN=Client/name=Fred Bloggs/emailAddress=This email address is being protected from spambots. You need JavaScript enabled to view it.
printf("Certificate verified successfully - belongs to %s\n", x->name);

// To get here, use ca.crt and client.crt
break;

// We ignore certificates that only fail because they have expired or aren't yet valid - not that anyone might
// still be around and playing the game when the certs expire but just that a permanent licence to the game is a
// permanent licence and we're only interested in a valid signature, not the date or a validity period.

case X509_V_ERR_CERT_HAS_EXPIRED:
printf("Certificate has expired. Ignoring.\n");
// To get here, use ca.crt and expired.crt, or fiddle the dates of check_x509_certificate
break;

case X509_V_ERR_CERT_NOT_YET_VALID:
printf("Certificate not yet valid. Ignoring.\n");
// To get here, use fiddle the dates of check_x509_certificate
break;

case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY:
printf("Certificate not valid - signing CA is missing or incorrect.\n");
// To get here, use fake_ca.crt and client.crt, or client.crt and client.crt
break;

default:
printf("Unhandled certificate verification error: %i - %s\n", error_return_value, X509_verify_cert_error_string(error_return_value));
// To get here, you really need to have messed things up.
break;
};

X509_STORE_CTX_free(csc);
ret = 0;
ret = (i > 0);
if(x != NULL)
X509_free(x);

return(ret);
}

CA certificates differ from client certificates only in their "purpose".  If they have a server / signing "purpose", we regard them as CA certificates, otherwise we regard them as client certificates.  We check this in the above code purely to stop people just copying the CA certificate to be the client certificate too (which would otherwise pass muster with this code).  There are myriad "purposes" that a certificate can have (actually X509_PURPOSE_get_count() of them) but given that we are misusing certificates anyway, we only check purpose 0 (as can be seen in the X509_PURPOSE_get0 line) which is "SSL".  The third parameter to check_purpose determines whether we are checking for client (0) or server/signing (1) capabilities.

// Returns TRUE if the given certificate is actually a CA certificate instead of a client one.
Boolean is_CA_certificate(X509\ *cert)
{
int id, ret;

// First index is for the "SSL" purpose, which we'll check for whether the cert has client or CA capabilities
id = X509_PURPOSE_get_id(X509_PURPOSE_get0(0);

// The "1" indicates "check for CA instead of client"
ret = X509_check_purpose(cert, id, 1);

// If the certificate is capable of acting as a CA (signer) for SSL, return TRUE.
return(ret==1);
};

The only other big code missing in there is the magical load_x509_certificate.  As mentioned, we do this from a file using OpenSSL's generic BIO interfaces but there are ways to do it from an in-memory certificate with BIO's too:

// Loads a PEM-encoded X509 certificate from the specified file.
X509 *load_x509_certificate(const char *file)
{
X509 *x = NULL;
BIO *cert;

// Create a new BIO file object
cert = BIO_new( BIO_s_file() );
if(cert == NULL)
{
printf("Could not read certificate %s into BIO\n", file);
return(NULL);
}

// Read the given file into the BIO object
if(BIO_read_filename(cert,file) <= 0)
{
printf("Could not read certificate %s\n", file);
return(NULL);
}

// Interpret the file as a PEM-encoded X509 certificate and return the certificate
x = PEM_read_bio_X509_AUX(cert, NULL, NULL, NULL);

if(cert != NULL)
BIO_free(cert);

return(x);
}

So what does this all mean? We can create certificates in a way prescribed for Apache SSL or OpenVPN using the tools available and an openssl compiled binary (available for most platforms), which gives us a CA which we can use to sign a client certificate for each user.  We distribute the application with the above code inside it (and appropriate checks on the return values, etc.), the public side of the CA certificate (not the key file!) and with a unique, signed client certificate for each user.  The code can then check if *we've* signed the client certificate, and if we have what data was in it when we signed it.  We can use this to display the user's name, address, etc. inside the game, or to perform further verification.  And we also know that only we can generate those certificates for users.

To bypass the system is only a matter of editing the program's code (which can be hindered in many ways) but, to the casual user, buying the game/application means you are given a unique file which contains their name and address and, short of delving into a debugger, they can't fake that file and allow the game to run.  There's lots of little side-lines to check (e.g. is the CA given the one you created, etc.) but those are relatively trivial to do.  However, it took me long enough to eek out and produce the above with the available documentation so it's a good start for a more complex program.