[Crypto] Licensing your software with OpenSSL. Using license keys. Software copy-protection.

You saw this many times: a license file with a name, serial number, features supported and some code. And some info of a computer that license is connected to, like MAC address.

Like:

Name=SpongeBob SquarePants
SN=123456789
Features=10101110011
MAC_Address=a2:fe:d6:79:d9:11
Sign=HYsXD1kNhT5wQtZonYi5OwIRMJe9x9wfUIa7m9pGZNKM/wufQ6uk4iq9xOc2JPV0HURYMG+/Oim9Fa7GftCgPNRei9mvnJEswIW450d3HbjJ9Pl7qk161jmzhpqci35dwOXjHhDcX0Qje0FvawxSEV81txwUrdGUnTHQoLTIOBkdzFjDfSEz8UzbJuya9BS6+AqEF+1D8tdXvBvcqf5JktAe6N4/T3sUhXPu5nhUMET3iPLHtOqjmWjDuL2k0RVfH6YiSEKRY065h+HLAS/W97Qbj/bRH5dkXt3ZFLYjdYDKqkm2vzLPnQBvYo/8TLYUMOhdlkA4pHaLgPVguvMquw==

How to do it using OpenSSL?

Command-line version

# Generate a RSA-2048 key
% openssl genrsa -out keypair.pem 2048

# Extract public key out of it:
% openssl rsa -in keypair.pem -pubout -out pubkey.pem

A sample license file for your customer:

% cat licence.txt

Name=SpongeBob SquarePants
SN=123456789
Features=10101110011
MAC_Address=a2:fe:d6:79:d9:11

Sign this text file with your RSA private key:

% openssl dgst -sign keypair.pem -keyform PEM -sha256 -out licence.txt.sign licence.txt

What's with "dgst" (digest) and "sha256" in command-line?

In fact, this is important thing to keep in your mind. RSA doesn't encrypt/sign your files. RSA encrypts/sign small pieces of data, smaller that it's key's length. Hence, RSA-2048 can encrypt/sign only data smaller than 2048 bits or 256 bytes. And the file is usually hashed using SHA256 or other algorithm. And the hash is then signed/verified by RSA.

After openssl's command execution, a 256-byte binary file licence.txt.sign is created. You can pass your customer a license: license.txt + licence.txt.sign. Well, the licence.txt.sign can be converted to base64, to be less intimidating:

% base64 licence.txt.sign

f2OUNTRNCQzehQOgqbsZZ4kSrQulY+bVouQeyB9v17zm0eqxhjIC3FHl05ChKcGmlAKyRI6xTB7G
MSX97Fn0bJ8rKFSZKfoB6Kj3wSH0V5OMM2nAqKUYt9HO8ZWjPuJrYb+Fmvwr8IkjiI0EV1OOR3m5
Kp/SMbrdR9LaN/iy3JFHFRGzku/RCrUJcwqzMnZKDyR0R+mztWZCgXqwSUg8yCQib/zqHEHChJRt
uFxYKBoqkioI/dAyRpLdxeQ5vswGK+DRZw4bEmSbSj1TmzEiWfZeuSh9DehGj6KpoBLNJZIKojWY
7fUlKTdvGAjjJ5/WXu6u3atIDOkXr7iPbDuSSA==

Now the verification.

Your software at customer's/users' side should check/verify this license. Here a public key comes to play.

% openssl dgst -verify pubkey.pem -keyform PEM -sha256 -signature licence.txt.sign -binary licence.txt
Verified OK

Change a byte in licence.txt, and it will not be verified.

It's important not to share your RSA private key with user. It must not be present in your software (a popular mistake, by the way). Otherwise a software cracker or reverse engineer would be able to generate licences for your software.

So at user's side there are only 3 files: pubkey.pem, licence.txt, licence.txt.sign. And OpenSSL library, of course.

Of course, you can use XML, JSON here, etc...

A version in pure C

Licence signing

#include <assert.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <openssl/sha.h>

const char* lic1="Name=SpongeBob SquarePants";
const char* lic2="SN=123456789";
const char* lic3="Features=10101110011";
const char* lic4="MAC_Address=a2:fe:d6:79:d9:11";

const char* pvt_key_fname="keypair.pem";

#define BUFFER_SIZE 512
static unsigned char buffer[BUFFER_SIZE];

int main(int argc, char *argv[])
{
    // Calculate SHA256 digest for datafile
    unsigned char digest[SHA256_DIGEST_LENGTH];
    SHA256_CTX ctx;
    SHA256_Init(&ctx);

    SHA256_Update(&ctx, lic1, strlen(lic1));
    SHA256_Update(&ctx, lic2, strlen(lic2));
    SHA256_Update(&ctx, lic3, strlen(lic3));
    SHA256_Update(&ctx, lic4, strlen(lic4));

    SHA256_Final(digest, &ctx);

    FILE* pvtkey = fopen(pvt_key_fname, "r");

    // Read pvt key from file
    RSA* rsa_pvtkey = PEM_read_RSAPrivateKey(pvtkey, NULL, NULL, NULL);

    unsigned bytes;
    int result = RSA_sign(NID_sha256, digest, SHA256_DIGEST_LENGTH, buffer, &bytes, rsa_pvtkey);
    assert (result==1); // success

    // 256-byte signature will fit:
    char encodedData[512];
    // https://wiki.openssl.org/index.php/Base64
    int n=EVP_EncodeBlock((unsigned char *)encodedData, buffer, bytes);
    assert (n<sizeof(encodedData));
    printf("%s\n", lic1);
    printf("%s\n", lic2);
    printf("%s\n", lic3);
    printf("%s\n", lic4);
    printf("Sign=%s\n", encodedData);

    RSA_free(rsa_pvtkey);
    fclose(pvtkey);
}

Compile it with OpenSSL library:

% gcc fname.c -lcrypto

(Ubuntu package libssl-dev is to be installed before.)

This keygen + RSA private key (keypair.pem) is to be kept in secret at your company.

This code generates licences, like:

Name=SpongeBob SquarePants
SN=123456789
Features=10101110011
MAC_Address=a2:fe:d6:79:d9:11
Sign=HYsXD1kNhT5wQtZonYi5OwIRMJe9x9wfUIa7m9pGZNKM/wufQ6uk4iq9xOc2JPV0HURYMG+/Oim9Fa7GftCgPNRei9mvnJEswIW450d3HbjJ9Pl7qk161jmzhpqci35dwOXjHhDcX0Qje0FvawxSEV81txwUrdGUnTHQoLTIOBkdzFjDfSEz8UzbJuya9BS6+AqEF+1D8tdXvBvcqf5JktAe6N4/T3sUhXPu5nhUMET3iPLHtOqjmWjDuL2k0RVfH6YiSEKRY065h+HLAS/W97Qbj/bRH5dkXt3ZFLYjdYDKqkm2vzLPnQBvYo/8TLYUMOhdlkA4pHaLgPVguvMquw==

Licence verification

This code takes licence file from stdin and checks it. It reads license data to be verified until it encounters "Sign".

#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <openssl/sha.h>

#define BUFFER_SIZE 512
static unsigned char buffer[BUFFER_SIZE];

int main(int argc, char *argv[])
{
    unsigned char digest[SHA256_DIGEST_LENGTH];
    SHA256_CTX ctx;
    SHA256_Init(&ctx);

    // Read data in chunks and feed it to OpenSSL SHA256
    while(fgets(buffer, BUFFER_SIZE, stdin)!=NULL)
    {
        // delete trailing newline:
        // https://stackoverflow.com/questions/2693776/removing-trailing-newline-character-from-fgets-input
        buffer[strcspn(buffer, "\r\n")] = 0;

        if (memcmp(buffer, "Sign=", 5)==0)
                break;
        SHA256_Update(&ctx, buffer, strlen(buffer));
    }

    SHA256_Final(digest, &ctx);

    // Verify that calculated digest and signature match
    FILE* pubkey = fopen("pubkey.pem", "r");
    // Read public key from file
    RSA* rsa_pubkey = PEM_read_RSA_PUBKEY(pubkey, NULL, NULL, NULL);
    fclose (pubkey);

    unsigned int pub_key_size_in_bytes=RSA_size(rsa_pubkey);

    unsigned char buffer2[512];
    assert (pub_key_size_in_bytes < sizeof(buffer2));
    int a=EVP_DecodeBlock(buffer2, buffer+5, strlen(buffer+5));
    assert (a>=pub_key_size_in_bytes); // may be slightly bigger

    int result = RSA_verify(NID_sha256, digest, SHA256_DIGEST_LENGTH,
                            buffer2, pub_key_size_in_bytes, rsa_pubkey);
    RSA_free(rsa_pubkey);

    if(result == 1)
        printf("Signature is valid\n");
    else
        printf("Signature is invalid\n");
}

A small problem: pubkey.pem is a file coming with your software. A cracker can crack all your system easily by generating his own pair of keys and replacing your file. He then will be able generate licences for your software, with that file replaced.

The public key may be embedded to a source code:

#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <openssl/sha.h>

#define BUFFER_SIZE 512
static unsigned char buffer[BUFFER_SIZE];

int main(int argc, char *argv[])
{
    unsigned char digest[SHA256_DIGEST_LENGTH];
    SHA256_CTX ctx;
    SHA256_Init(&ctx);

    while(fgets(buffer, BUFFER_SIZE, stdin)!=NULL)
    {
        // delete trailing newline:
        // https://stackoverflow.com/questions/2693776/removing-trailing-newline-character-from-fgets-input
        buffer[strcspn(buffer, "\r\n")] = 0;

        if (memcmp(buffer, "Sign=", 5)==0)
                break;
        SHA256_Update(&ctx, buffer, strlen(buffer));
    }

    SHA256_Final(digest, &ctx);

    // https://stackoverflow.com/questions/7818117/why-i-cant-read-openssl-generated-rsa-pub-key-with-pem-read-rsapublickey
    // newlines must be present!
    // may be obfuscated, XOR-ed, etc
    const char* pubkey=
"-----BEGIN RSA PUBLIC KEY-----\n"
"MIIBCgKCAQEA35l1g3wwa4n1efx7X77rj3/FM/fNQ03wUFbwLzCWnSWQeJde6JGI\n"
"T2PW+CoemmD8z69lIn6r5srSYyQ8Ngs6rXQ7TMZb/9nULeLn0xJ4VItYZZxYc2gE\n"
"nr6TRaOa/GAxLdE/jSePT7FOOTUzNZvvSETySOLdeiyXL12WLXZI3UoSQl6AYrfQ\n"
"LoTfcDxZ3eVDxKXEkt5Ff8UszPbOyTpaGIMxW/HWB5AYP8HXNfchXlFkuYdnXMk0\n"
"Lu8sN+t03xCo/BiIROhSDNjVNorGo3O2JavZYu3ns0P/y52xfbFKVxVIePjLITIx\n"
"rldP4vMbggPoIfCGv8e0aR9DNSOpflkZmQIDAQAB\n"
"-----END RSA PUBLIC KEY-----\n";

    // https://stackoverflow.com/questions/51672133/what-are-openssl-bios-how-do-they-work-how-are-bios-used-in-openssl
    BIO* bio = BIO_new(BIO_s_mem());
    size_t written;
    int tmp=BIO_write_ex(bio, pubkey, strlen(pubkey), &written);
    assert (tmp==1); // success
    RSA* rsa_pubkey=PEM_read_bio_RSAPublicKey(bio, NULL, 0, NULL);
    assert (rsa_pubkey!=NULL);

    unsigned int pub_key_size_in_bytes=RSA_size(rsa_pubkey);

    unsigned char buffer2[512];
    assert (pub_key_size_in_bytes < sizeof(buffer2));
    int a=EVP_DecodeBlock(buffer2, buffer+5, strlen(buffer+5));
    assert (a>=pub_key_size_in_bytes); // may be slightly bigger

    int result = RSA_verify(NID_sha256, digest, SHA256_DIGEST_LENGTH, buffer2, pub_key_size_in_bytes, rsa_pubkey);
    RSA_free(rsa_pubkey);

    if(result == 1)
        printf("Signature is valid\n");
    else
        printf("Signature is invalid\n");
}

Please note: a public key must be converted, to be compatible with the PEM_read_bio_RSAPublicKey() function.

This can be cracked as well: a cracker can just patch all the executables and replace the public key to his own. This can be even done automatically. A key here can be obfuscated slightly, this will slow down his efforts for a little, but still, not a panacea.

Bad news: ways of cracking

A public key can be replaced by the one generated by a cracker.

Anyway, a cracker can just patch conditional jump to bypass license check.

Another problem: while OpenSSL is a solid library, it contains too many strings related to cryptoalgorithms, all the error messages, etc. This is a sure sign for a cracker that something related to crypto is going on right here. He will quickly find out, where to dig into.

Some better ways of protecting your software from copying.

Good news

Without patching, a keygen for your software is literally impossible, unless a software cracker can factor RSA-2048.

But this is a way better than implementing your own amateur cryptography that developers often do for their licensing libraries. Keygens for "amateur cryptography" are often possible.

This protection is no worse than the popular FLEXlm licensing manager -- same level of protection, but can be implemented in couple of small functions. The only thing I omitted is a MAC address check.

Other notes

Sign's length is the same as a length of your RSA key. The bigger key, the better secrecy, but the longer the sign.

Some people see that a long sign or license key is bad. There was a time when license keys were transfered by phone, fax, etc. But today, in the Internet epoch, this is not the issue anymore

I used several functions that are deprecated already in OpenSSL: RSA_sign, PEM_verify. It's recommended to use other OpenSSL functions: EVP_PKEY_sign_init, EVP_PKEY_sign, EVP_PKEY_verify_init and EVP_PKEY_verify. But these, "newer" functions hashes data and sign/verify using the same function call.

On the other hand, I wanted to stress the fact that SHA256 hashing and RSA signing/verifying are separate steps. Of course, you should use these "newer" functions in your code.

My C code is based on the code I stolen from the PAGE FAULT BLOG and reworked -- thanks to him.


UPD: as seen on reddit.


List of my other blog posts.

Yes, I know about these lousy Disqus ads. Please use adblocker. I would consider to subscribe to 'pro' version of Disqus if the signal/noise ratio in comments would be good enough.