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?
# 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...
#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==
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.
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.
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.
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.
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.