Product Activation Based on RSA Signatures

Introduction

This article will present the reader with a framework for Product Activation. It is a logical conclusion to Product Keys Based on the Advanced Encryption Standard and Product Keys Based on Elliptic Curve Cryptography. The final portion of the series will discuss the following:

  • RSA Cryptography
  • Compiling and Integrating Crypto++ into the Microsoft Visual C++ Environment
  • Product Activation
  • Named Pipe Client/Server
  • Generating and Serializing RSA Keys
  • RSA Signing and Verification Functions
  • Product Key Signing

RSA Cryptography

RSA is the work of Ron Rivest, Adi Shamir, and Leonard Adleman. The system was developed in 1977 and patented by the Massachusetts Institute of Technology. Although Rivest, Shamir, and Adleman are generally credited with the discovery, Clifford Cocks (Chief Mathematician at GCHQ—the British equivalent of the NSA) described the system in 1973. However, Cocks did not publish (the work was considered classified), so the credit lies with Rivest, Shamir, and Adleman.

The RSA patent expired in September of 2000, and was subsequently placed in Public Domain. See RSA Security's Press Release.

Compiling and Integrating Crypto++ into the Microsoft Visual C++ Environment

Please see the related article, "Compiling and Integrating Crypto++ into the Microsoft Visual C++ Environment." This article is based upon basic assumptions presented in the previously mentioned article. It also addresses most problems encountered with projects from Command Line to MFC (Errors C1083, C1189, LNK1104, LNK2001, and LNK2005). Additionally, it provides some tips and other nicities for using the Crypto++ Library.

Product Activation Based on RSA Signatures

Product Activation

Product Activation is the process of validating the Product Key. Software authors may desire activation for any number of reasons. The two most prevalent appear to be thwarting piracy and developing end user demographics.

In the Piracy arena, an Activation Server can:

  • Detect circulated Product Keys using statistical methods
  • Enforce renewal periods by removing the system time functions from the end user's computer

Microsoft uses Product Acivation to curb Piracy. Taking from the Microsoft Activation FAQ:

Product Activation works by validating that the software's product key, required as part of product installation, has not been used on more PCs than is allowed by the software's end user license agreement (EULA).

... Microsoft designed Product Activation as a simple way to verify the software license and thwart the spread of software piracy.

This implies Product Activation is an ongoing or recurring process. For the purposes of this article, Product Activation workflow will proceed as depicted below.

Product Activation Based on RSA Signatures

Named Pipe Client/Server

To provide a realistic implementation, this article will use Named Pipes in blocking mode as the underlying communication. Named Pipes is an interprocess communication mechanism thatch is easy to implement so the reader can focus on the cryptographic functions without the distraction of asynchronous socket programming. Note that Q177696, How To Use Named Pipes in a Visual Basic 32-bit Program is the most concise documentation for using Named Pipes the author has found.

The author chose a single Workspace with two projects: a Client Project and a Sever Project.

At times, it is a bit inconvenient because a switch must occur to the 'Active Project' (by way of the Project menu shown below).

The first step in the Product Activation Server implementation is developing an echo server based on Named Pipes. The following points should be observed with respect to the following code:

  • Only the Server creates the Named Pipe using CreateNamedPipe()
  • The Server must be running Windows NT or above
  • The Client opens the Named Pipe by using CreateFile()
  • The client can be any 32-bit version of the Windows OS
  • The pipe is bidirectional by way of PIPE_ACCESS_DUPLEX
  • The Server and Client connect to the Named Pipe by Handle
  • The Server and Client read data from the pipe using ReadFile()
  • The Server and Client write data to the pipe
  • using WriteFile()
  • \\.\Pipe\ specifies the Local Machine

The Named Pipe is created by the Server as follows:

bool PipeCreate( HANDLE& pipe )
{
   pipe = CreateNamedPipe( _T("\\\\.\\Pipe\\Product Activation Test"),
                           PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE |
                           PIPE_WAIT, PIPE_UNLIMITED_INSTANCES,
                           BUFFER_SIZE, BUFFER_SIZE,
                           PIPE_TIMEOUT, NULL );

   return( INVALID_HANDLE_VALUE != pipe );
}

Once the pipe is created, the Server listens for a connection. Note that PipeRead blocks waiting for a Client connection because the underlying ConnectNamedPipe() is synchronous.

int main(int argc, char* argv[])
{
   HANDLE pipe = INVALID_HANDLE_VALUE ;
   if( false == PipeCreate( pipe ) )
   { ... }

   std::string Recieved;
   if( false == PipeRead( pipe, Recieved ) )
   { ... }

   return 0;
}

ReadPipe() is shown below. All information sent across the pipe will be Base64 encoded. However, as a safety, cbBuffer[ dwRead ] = _T( '\0' ) is performed to assure a NULL terminated string.

bool PipeRead( const HANDLE& pipe, std::string& Recieved )
{
   bool result = ( TRUE == ConnectNamedPipe( pipe, NULL ) );
   if( false == result && ERROR_PIPE_CONNECTED != GetLastError() )
      { return false; }

   byte cbBuffer[ BUFFER_SIZE ];
   DWORD dwRead = -1;
   result = ( TRUE == ReadFile( pipe, cbBuffer,
              BUFFER_SIZE, &dwRead, NULL ) );

   cbBuffer[ dwRead ] = _T( '\0' );

   FlushFileBuffers( pipe );

   return 0 != dwRead;
}

PipeWrite will be coded as follows. Message.length() + 1 is used to capture the NULL termination.

bool PipeWrite( const HANDLE& pipe, const std::string& Message )
{
   bool result = ( TRUE == ConnectNamedPipe( pipe, NULL ) );
   if( false == result && ERROR_PIPE_CONNECTED != GetLastError() )
      { return false; }

   DWORD dwWritten = -1;
   result = ( TRUE == WriteFile( pipe, Message.c_str(),
              Message.length() + 1, &dwWritten, NULL ) );

   return( dwWritten == Message.length() + 1 );
}

The final Echo Server code is shown following the results of running Server.

// Server.cpp

bool PipeCreate( HANDLE& pipe );
bool PipeRead( const HANDLE& pipe, std::string& Recieved );
bool PipeWrite( const HANDLE& pipe, const std::string& Message );

void DebugMessage( const std::string& message, bool ExtraCRLF = false );

int main(int argc, char* argv[])
{
   HANDLE pipe = INVALID_HANDLE_VALUE ;

   try {

      if( false == PipeCreate( pipe ) )
          { throw std::string( "Server: CreateNamedPipe returned
                                INVALID_HANDLE_VALUE" ); }

      std::string Recieved;;
      if( false == PipeRead( pipe, Recieved ) )
            { throw( std::string( _T("Server:
              PipeRead returned false") ) ); }

      if( false == PipeWrite( pipe, Recieved ) )
            { throw( std::string( _T("Server: WriteRead
              returned false") ) ); }

      // If the program exits, the Client will receive
      //    ERROR_FILE_NOT_FOUND.
      while( true ) { 
            Sleep( 5 * 1000 );
      }
   }

   catch( CryptoPP::Exception& e ) {
      std::cerr << "Error: " << e.what() << std::endl;
   }

   catch( std::string& s ) {
      std::cerr << "Error: " << s << std::endl;
   }

   catch( TCHAR* s ) {
      std::cerr << "Error: " << s << std::endl;
   }

   catch (...) {
      std::cerr << "Unknown Error" << std::endl;
   }

   if( NULL != pipe ) { CloseHandle( pipe ); }

   return 0;
}

bool PipeWrite( const HANDLE& pipe, const std::string& Message )
{
   bool result = ( TRUE == ConnectNamedPipe( pipe, NULL ) );
   if( false == result && ERROR_PIPE_CONNECTED != GetLastError() )
      { return false; }

   DWORD dwWritten = -1;
   result = ( TRUE == WriteFile( pipe, Message.c_str(),
                                 Message.length() + 1,
                                 &dwWritten, NULL ) );

   FlushFileBuffers( pipe );

   // Do not call DisconnectNamedPipe()
   //   The client will receive ERROR_PIPE_BUSY
   //   or ERROR_PIPE_NOT_CONNECTED

   DebugMessage( "Server: Sent:" );
   DebugMessage( Message );

   return( dwWritten == Message.length() + 1 );
}

bool PipeRead( const HANDLE& pipe, std::string& Received )
{
   DebugMessage( "Server: Waiting for Client Connection"  );

   bool result = ( TRUE == ConnectNamedPipe( pipe, NULL ) );
   if( false == result && ERROR_PIPE_CONNECTED != GetLastError() )
      { return false; }

   byte cbBuffer[ BUFFER_SIZE ];
   DWORD dwRead = -1;
   result = ( TRUE == ReadFile( pipe, cbBuffer,
                                BUFFER_SIZE, &dwRead, NULL ) );

   cbBuffer[ dwRead ] = _T( '\0' );
   Received = (char*)cbBuffer;

   DebugMessage( _T( "Server: Received:" ) );
   DebugMessage( Received );

   FlushFileBuffers( pipe );

   // Do not call DisconnectNamedPipe()
   //   The client will receive ERROR_PIPE_BUSY
   //   or ERROR_PIPE_NOT_CONNECTED

   return( 0 != dwRead );
}

bool PipeCreate( HANDLE& pipe )
{
   pipe = CreateNamedPipe( _T("\\\\.\\Pipe\\ProductActivationTest"),
                           PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE |
                           PIPE_WAIT, PIPE_UNLIMITED_INSTANCES,
                           BUFFER_SIZE, BUFFER_SIZE, PIPE_TIMEOUT,
                           NULL );

   return( INVALID_HANDLE_VALUE != pipe );
}

void DebugMessage( const std::string& message, bool ExtraCRLF )
{
   std::cout << message << std::endl;

   if( true == ExtraCRLF )
      { std::cout << std::endl; }
}

Product Activation Based on RSA Signatures

Named Pipe Client/Server (continued)

The Client program is very similar to the Server—the difference being the Client uses PipeOpen() rather than PipeCreate(). The perceptive reader will realize that the communication portion of the Proof of Concept is complete. Recall that the Client must send the Product Key to the Server, and the Server must send the Signed Product Key back to the Client.

// Client.cpp

bool PipeOpen( HANDLE& pipe );
bool PipeRead( const HANDLE& pipe, std::string& Recieved );
bool PipeWrite( const HANDLE& pipe, const std::string& Message );

void DebugMessage( const std::string& message, bool ExtraCRLF = false );

int main(int argc, char* argv[])
{
   HANDLE pipe = INVALID_HANDLE_VALUE ;

   try {

      if( false == PipeOpen( pipe ) )
          { throw( std::string( _T("Client: PipeOpen returned
                                    false") ) ); }

      std::string Message = "Hello World";
      if( false == PipeWrite( pipe, Message ) )
          { throw( std::string( _T("Client: PipeWrite returned
                                    false") ) ); }

      std::string Received;
      if( false == PipeRead( pipe, Received ) )
          { throw( std::string( _T("Client: PipeRead returned
                                    false") ) ); }
   }

   catch( CryptoPP::Exception& e ) {
      std::cerr << "Error: " << e.what() << std::endl;
   }

   catch( std::string& s ) {
      std::cerr << "Error: " << s << std::endl;
   }

   catch( TCHAR* s ) {
      std::cerr << "Error: " << s << std::endl;
   }

   catch (...) {
      std::cerr << "Unknown Error" << std::endl;
   }

   if( NULL != pipe ) { CloseHandle( pipe ); }

   return 0;
}

bool PipeWrite( const HANDLE& pipe, const std::string& Message )
{
   DWORD dwWritten = 0;
   bool result = ( TRUE == WriteFile( pipe, Message.c_str(),
                                      Message.length() + 1,
                                      &dwWritten, NULL ) );

   FlushFileBuffers( pipe ); 

   DebugMessage( "Client: Sent:" );
   DebugMessage( Message );

   return( dwWritten == Message.length() + 1 );
}

bool PipeRead( const HANDLE& pipe, std::string& Received )
{
   byte cbBuffer[ BUFFER_SIZE ];
   DWORD dwRead = -1;
   bool result = ( TRUE == ReadFile( pipe, cbBuffer,
                                     BUFFER_SIZE, &dwRead, NULL ) );

   cbBuffer[ dwRead ] = _T( '\0' );
   Received = (char*)cbBuffer;

   DebugMessage(  _T( "Client: Received:" ) );
   DebugMessage( Received );

   return 0 != dwRead;
}

bool PipeOpen( HANDLE& pipe )
{
   // STANDARD_RIGHTS_READ | STANDARD_RIGHTS_WRITE
   //   is too strong - ERROR_ACCESS_DENIED
   pipe = CreateFile( _T("\\\\.\\Pipe\\ProductActivationTest"),
                             GENERIC_READ | GENERIC_WRITE, 0,
                             NULL, OPEN_EXISTING, 0, NULL);

   if( INVALID_HANDLE_VALUE != pipe )
   {
        DebugMessage( "Client: Opened Named Pipe" );
   }

   return( INVALID_HANDLE_VALUE != pipe );
}

void DebugMessage( const std::string& message, bool ExtraCRLF )
{
   std::cout << message << std::endl;

   if( true == ExtraCRLF )
        { std::cout << std::endl; }
}

Product Activation Based on RSA Signatures

Generating and Serializing RSA Keys

This portion of the article will accomplish the following tasks:

  • Generate an RSA Key Pair
  • Export the Private Key (used by the Server)
  • Import the Public Key (used by the Client)

If the reader would like another treatment of RSA and Signing, he or she should visit Victor Volkman's "Crypto++® Holds the Key to Encrypting Your C++ Application Data."

RSA keys are generated for use in the Signing function. Should the user want to encrypt the Product Key before transmission for Signing by the Server (and return to the Client), the reader should: 1) use a Symmetric Ciper such as AES; or 2) use SSL.

Most operations on the RSA Keys will be perform by way of Crypto++'s RSAFunction class. The inheritance diagram is shown above. The reader is encouraged to view rsa.h at this point. Functions of interest in the class include:

  • GenerateRandom()
  • GetPrime1()
  • GetPrime2()
  • GetPrivateExponent()

In addition, useful typedef are provided:

// The two RSA encryption schemes defined in PKCS #1 v2.0
typedef RSAES<PKCS1v15>::Decryptor RSAES_PKCS1v15_Decryptor;
typedef RSAES<PKCS1v15>::Encryptor RSAES_PKCS1v15_Encryptor;

typedef RSAES<OAEP<SHA> >::Decryptor RSAES_OAEP_SHA_Decryptor;
typedef RSAES<OAEP<SHA> >::Encryptor RSAES_OAEP_SHA_Encryptor;

// The three RSA signature schemes defined in PKCS #1 v2.0
typedef RSASS<PKCS1v15, SHA>::Signer RSASSA_PKCS1v15_SHA_Signer;
typedef RSASS<PKCS1v15, SHA>::Verifier RSASSA_PKCS1v15_SHA_Verifier;

typedef RSASS<PKCS1v15, MD2>::Signer RSASSA_PKCS1v15_MD2_Signer;
typedef RSASS<PKCS1v15, MD2>::Verifier RSASSA_PKCS1v15_MD2_Verifier;

typedef RSASS<PKCS1v15, MD5>::Signer RSASSA_PKCS1v15_MD5_Signer;
typedef RSASS<PKCS1v15, MD5>::Verifier RSASSA_PKCS1v15_MD5_Verifier;

Finally, test.cpp provides the following functions from the Crypto++ test harness:

  • GenerateRSAKey()
  • RSAEncryptString()
  • RSADecryptString()
  • RSASignFile()
  • RSAVerifyFile()

Product Activation Based on RSA Signatures

Generating and Serializing RSA Keys (continued)

The first step in this segment is to generate a key pair (the Serialization is realized at no cost due to Sinks). This is accomplished as follows.

std::string PrivateKeyFile = "key.pv";
std::string PublicKeyFile  = "key.pb";

RSAES_OAEP_SHA_Decryptor Decryptor(randPool, 0);
HexEncoder privFile(new FileSink( PrivateKeyFile.c_str() ));
Decryptor.DEREncode(privFile);
privFile.MessageEnd();

RSAES_OAEP_SHA_Encryptor Encryptor(Decryptor);
HexEncoder pubFile(new FileSink( PrivateKeyFile.c_str() ));
Encryptor.DEREncode(pubFile);
pubFile.MessageEnd();

To overcome the modulus issue, perform the following. Also note that AutoSeededRandomPool is now being used rather than RandomPool since the RandomPool is not being seeded by the passphrase.

AutoSeededRandomPool rng;

// Specify 512 bit modulus, accept e = 17
RSAES_OAEP_SHA_Decryptor Decryptor( rng, 512 /*, e */ );

By using File Sinks (Sources and Sinks were discussed in "Product Keys Based on the Advanced Encryption Standard"), one does not have to write Serialization code.

The full implementation of patest2 follows. Below are the typeical contents of the Key Files.

Public Key Serialization
Private Key Serialization
// patest2.cpp

#include "rsa.h"
#include "osrng.h"    // PRNG
#include "hex.h"      // Hex Encoder/Decoder
#include "files.h"    // File Source and Sink

int main(int argc, char* argv[])
{
   try
   {
      std::string PrivateKeyFile = "key.pv";
      std::string PublicKeyFilen  = "key.pb";

      CryptoPP::AutoSeededRandomPool rng;

      // Specify 512 bit modulus, accept e = 17
      CryptoPP::RSAES_OAEP_SHA_Decryptor Decryptor( rng, 512 /*, e */ );
      CryptoPP::HexEncoder privFile(new
            CryptoPP::FileSink( PrivateKeyFile.c_str() )
      ); // Hex Encoder

      Decryptor.DEREncode(privFile);
      privFile.MessageEnd();

      CryptoPP::RSAES_OAEP_SHA_Encryptor Encryptor(Decryptor);
      CryptoPP::HexEncoder pubFile(new
         CryptoPP::FileSink( PublicKeyFile.c_str() )
      ); // Hex Encoder

      Encryptor.DEREncode(pubFile);
      pubFile.MessageEnd();
   }

   catch( CryptoPP::Exception& e ) {
      std::cerr << "Error: " << e.what() << std::endl;
   }

   catch( std::string& s ) {
      std::cerr << "Error: " << s << std::endl;
   }

   catch (...) {
      std::cerr << "Unknown Error" << std::endl;
   }

   return 0;
}

Product Activation Based on RSA Signatures

Generating and Serializing RSA Keys (continued)

Retrieving the key from the keyfile would be accomplished as follows. patest3 loads the previously saved Public and Private Key pairs. Prior to running the sample code, the reader should copy key.pb and key.pv to the current working directory. It is noteworthy that the keys are not encrypted on the disk. It is left as an exercise for the reader.

string PrivateKeyFile = "key.pv";
string PublicKeyFile  = "key.pb";

FileSource pubFile( PublicKeyFile.c_str(),
                    true, new HexDecoder );

FileSource privFile( PrivateKeyFile.c_str(),
                     true, new HexDecoder);

RSAES_OAEP_SHA_Encryptor Encryptor ( pubFile );
RSAES_OAEP_SHA_Decryptor Decryptor( privFile );

The source to patest3 is shown after the sample run.

// patest3.cpp

#include "rsa.h"
#include "osrng.h"    // PRNG
#include "hex.h"      // Hex Encoder/Decoder
#include "files.h"    // File Source and Sink

int main(int argc, char* argv[])
{
   try
   {
      std::string PrivateKeyFile = "key.pv";
      std::string PublicKeyFile  = "key.pb";

      CryptoPP::FileSource pubFile( PublicKeyFile.c_str(),
                                    true, new CryptoPP::HexDecoder );

      CryptoPP::FileSource privFile( PrivateKeyFile.c_str(),
                                     true, new CryptoPP::HexDecoder);

      CryptoPP::RSAES_OAEP_SHA_Decryptor Decryptor( privFile );
      CryptoPP::RSAES_OAEP_SHA_Encryptor Encryptor ( pubFile );

      std::cout << "RSA Parameters" << std:: endl << std:: endl;

      std::cout << "modulus: "
                << Encryptor.GetTrapdoorFunction().GetModulus();
      std::cout << std::endl << std::endl;

      std::cout << "e: "
                << Encryptor.GetTrapdoorFunction().GetPublicExponent();
      std::cout << std::endl << std::endl;

      std::cout << "p: "
                << Decryptor.GetTrapdoorFunction().GetPrime1();
      std::cout << std::endl << std::endl;

      std::cout << "q: "
                << Decryptor.GetTrapdoorFunction().GetPrime2();
      std::cout << std::endl << std::endl;

      std::cout << "d: "
                << Decryptor.GetTrapdoorFunction().GetPrivateExponent();
      std::cout << std::endl << std::endl;

   }

   catch( CryptoPP::Exception& e ) {
      std::cerr << "Error: " << e.what() << std::endl;
   }

   catch( std::string& s ) {
      std::cerr << "Error: " << s << std::endl;
   }

   catch (...) {
      std::cerr << "Unknown Error" << std::endl;
   }

   return 0;
}

Product Activation Based on RSA Signatures

RSA Signing and Verification Functions

The Crypto++ Library provides the user with sample code by way of the validation routines. Two such samples are RSASignFile and RSASignFile (located in test.cpp). The provided code will be the base for the Signing and Verification. However, rather than operating on Files, patest4 will operate on an in memory Message string. Note that the Signature is still written to a file.

// patest4

// PRNG
AutoSeededRandomPool rng;

// Message M
//   AES based Product Key
std::string message = "X3BA-9NSF-8N9Q-UWQC-U7FX-AZZF-JAJW";

// Output (Signed) File
std::string SignedFile = "message.sig";

// Private Key
std::string PrivateKeyFile = "key.pv"; 

CryptoPP::FileSource privFile( PrivateKeyFile.c_str(), true,
                               new CryptoPP::HexDecoder);
RSASSA_PKCS1v15_SHA_Signer priv(privFile);
StringSource s1(message, true,
                    new SignerFilter( rng, priv,
                        new HexEncoder(
                            new FileSink( SignedFile.c_str() )
                        )    // HexEncoder
                    )        // SignerFilter
               );            // StringSource

Product Activation Based on RSA Signatures

RSA Signing and Verification Functions (continued)

Verification is a bit more complicated, but not insurmountable by any means. Again, the Message M is in memory rather than on disk.

// patest4

// Public Key
std::string PublicKeyFile = "key.pb";

// Load Key
FileSource pubFile( PublicKeyFile.c_str(), true,
                    new CryptoPP::HexDecoder );
RSASSA_PKCS1v15_SHA_Verifier pub(pubFile);

// Intialize File Source
FileSource signatureFile(SignedFile.c_str(), true, new HexDecoder);

// Sanity Check
if (signatureFile.MaxRetrievable() != pub.SignatureLength())
   { throw std::string( "Signature File Size Problem" ); }

SecByteBlock signature(pub.SignatureLength());
signatureFile.Get(signature, signature.size());

// Prepare Verifier
VerifierFilter *verifierFilter = new VerifierFilter(pub);
verifierFilter->Put(signature, pub.SignatureLength());

// Invoke Verifier
StringSource s2(message, true, verifierFilter);

// Paydirt
if( false == verifierFilter->GetLastResult() )
   { throw std::string( "Signature Verification Failed" ); }

std::cout << "Signature Verified" << std::cout;

Note that the following (more elegant) is not available due to requiring the result of verifierFilter->GetLastResult():

StringSource s2(message, true,
                new VerifierFilter( pub,
                    new HexDecoder(
                       new FileSink( SignedFile.c_str() )
                       )    // HexEncoder
                   )        // VerifierFilter
               );           // StringSource

Product Activation Based on RSA Signatures

RSA Signing and Verification Functions (continued)

// patest4.cpp

#include "rsa.h"
#include "osrng.h"    // PRNG
#include "hex.h"      // Hex Encoder/Decoder
#include "files.h"    // File Source and Sink

int main(int argc, char* argv[])
{
   try
   {
      // PRNG
      AutoSeededRandomPool rng;

      // Message M
      std::string message = "X3BA-9NSF-8N9Q-UWQC-U7FX-AZZF-JAJW";

      // Output (Signed) File
      std::string SignedFile = "message.sig";

      //////////////////////////////////////////
      //                Signing               //
      //////////////////////////////////////////

      // Private Key
      std::string PrivateKeyFile = "key.pv"; 

      CryptoPP::FileSource privFile( PrivateKeyFile.c_str(), true,
                                     new CryptoPP::HexDecoder);
      RSASSA_PKCS1v15_SHA_Signer priv(privFile);
      StringSource s1(message, true,
                         new SignerFilter( rng, priv,
                             new HexEncoder(
                                 new FileSink( SignedFile.c_str() )
                             )    // HexEncoder
                         )        // SignerFilter
                     );           // StringSource

      //////////////////////////////////////////
      //                  DMZ                 //
      //////////////////////////////////////////

      // Public Key
      std::string PublicKeyFile = "key.pb";

      //////////////////////////////////////////
      //              Validation              //
      //////////////////////////////////////////

      FileSource pubFile( PublicKeyFile.c_str(), true,
                          new CryptoPP::HexDecoder );
      RSASSA_PKCS1v15_SHA_Verifier pub(pubFile);

      FileSource signatureFile(SignedFile.c_str(), true, new HexDecoder);
      if (signatureFile.MaxRetrievable() != pub.SignatureLength())
          { throw std::string( "Signature File Size Problem" ); }

      SecByteBlock signature(pub.SignatureLength());
      signatureFile.Get(signature, signature.size());

      VerifierFilter *verifierFilter = new VerifierFilter(pub);
      verifierFilter->Put(signature, pub.SignatureLength());
      StringSource s2(message, true, verifierFilter);

      if( false == verifierFilter->GetLastResult() )
          { throw std::string( "Signature Verification Failed" ); }

      std::cout << "Signature Verified" << std::cout;
   }

   catch( CryptoPP::Exception& e )
      { std::cerr << "Error: " << e.what() << std::endl; }

   catch( std::exception& e )
      { std::cerr << "Error: " << e.what() << std::endl; }

   catch( std::string& s )
      { std::cerr << "Error: " << s << std::endl; }

   catch (...)
      { std::cerr << "Unknown Error" << std::endl; }

   return 0;
}

Product Activation Based on RSA Signatures

Product Key Signing

The software author will have to determine what to send, and how to send it. Just as previous examples have interpreted the string based on a predefined format, so must the vendor's implementation. This would include control messages and data. For simplicity, it is recommended that control messages be sent in band (in other words, as a string concatenation).

Unique installations can be tracked by way of a hashing of end user hardware components. To provide tolerance, the Activation logic should allow for hardware changes (in other words, allow X components to change before invoking a reactivation).

Additionally, no personally identifiable or private information should be sent. Current systems implement this by discarding portions of each hash. This has the net effect of lessening the collision domain, but the probabilistic chance of hardware changes mapping to themselves are very small. For example, suppose one creates a 32-bit digest of the Network Adapter MAC Address, IDE Adapter, and Display Adapter. Discard the high-order 26 bits of each hash, and use the low order 6 bits for identification purposes. Given that one knows the low order-6 bits, one cannot determine the Adapter because many Adapters will obtain a similar hash.

The reader should also consider assigning a relative weight to the value of each hash. For example, RAM amount probably changes more frequently than CPU speed. So, a change in RAM should affect the system less than a change in CPUs.

Much has been written on Windows Product Activation with the release of Windows Vista. WPA employs these techniques. The reader should further investigate the Microsoft Scheme to increase his or her understanding. The author is aware of papers circulating which was considered an accurate Reverse Engineering attempt by GmbH in July, 2001. Note that this was with respect to Windows XP RC1.

For the purposes of this article, only the Product Key will be sent to the Server to be Signed. The Signature is sent back to the Client, not the {Key, Signature} tuple.

Product Activation Based on RSA Signatures

Product Key Signing (continued)

The following two outputs show the results of patest5. patest5 Base64 Encodes the Product Key; and receives the same product key from the server. The changes to the Client are listed below (the Server is similar).

Client

Server

// patest5

if( false == PipeOpen( pipe ) )
   { throw( std::string( _T("Client: PipeOpen returned false") ) ); }

std::string Message = "X3BA-9NSF-8N9Q-UWQC-U7FX-AZZF-JAJW";
std::string Base64Encoded;
CryptoPP::StringSource( Message, true,
                            new CryptoPP::Base64Encoder( 
                                new CryptoPP::StringSink( Base64Encoded )
                            ) // Base64Encoder
                       ); // StringSource

if( false == PipeWrite( pipe, Base64Encoded ) )
   { throw( std::string( _T("Client: PipeWrite returned false") ) ); }

std::string Received;
if( false == PipeRead( pipe, Received ) )
   { throw( std::string( _T("Client: PipeRead returned false") ) ); }

std::string Base64Decoded;
CryptoPP::StringSource( Received, true,
                            new CryptoPP::Base64Decoder( 
                                new CryptoPP::StringSink( Base64Decoded )
                            ) // Base64Encoder
                        ); // StringSource

std::cout << "Received from Server:" << std::endl;
std::cout << "  " << Base64Decoded << std::endl;

Product Activation Based on RSA Signatures

Product Key Signing (continued)

patest6 culminates this article. The Server program uses the Private Key to Sign the Product Key (not Encrypt), and the Client program uses the Public Key to Verify the Signature. The source code is a melding of the previous examples presented.

Client: Successful Product Activation
Server: Successful Product Activation

Should the user desire to test a failed Activation, set bool Valid = true. This will cause the Server to sSign a NULL Key ( literally "NULL-NULL-NULL-NULL-NULL-NULL-NULL"). When the Client Verifies the Signature, S(M) ≠ S(NULL), so the Activation fails.

Failed Activation

Both the Client and the Server share common function implementations. For example, Base64Encode() and Base64Decode(). The Server solely uses SignMessage(), whereas the same is true for VerifySignature() with respect to the Client.

Product Activation Based on RSA Signatures

Product Key Signing (continued)

The following is the flow of logic through the Server.

const std::string NullKey = "NULL-NULL-NULL-NULL-NULL-NULL-NULL";

int main(int argc, char* argv[])
{
   HANDLE pipe = INVALID_HANDLE_VALUE ;

   try {

      if( false == PipeCreate( pipe ) )
         { throw std::string( "Server: PipeCreate returned false" ); }

      std::string Received;;
      if( false == PipeRead( pipe, Received ) )
         { throw( std::string( _T("Server: PipeRead returned
                                   false") ) ); }

      //////////////////////////////////////////
      //             Base64 Decode            //
      //////////////////////////////////////////
      std::string Base64Decoded;
      Base64Decode( Received, Base64Decoded );

      //////////////////////////////////////////
      //         Transmission Control         //
      //             and Tampering            //
      //////////////////////////////////////////
      //
      // Decrypt Product Key
      // Is it a Valid Encryption?
      //   Does Magic = 0xAAAAAAAA?

      //////////////////////////////////////////
      //              Statistics              //
      //////////////////////////////////////////
      //
      // Add User Information to Demographics
      //   Database for Data Mining

      //////////////////////////////////////////
      //             Verification             //
      //////////////////////////////////////////
      //
      // Check Key X
      //   Was it Generated by the Software Vendor?
      //
      // Check number of Activations of Key X
      //   Is it within allowable limits?
      bool Valid = true;

      //////////////////////////////////////////
      //                Signing               //
      //////////////////////////////////////////
      //
      // if true == Valid
      //   Transmit S(K)
      // else
      //   Transmit S(NULL Key)

      // Private Key
      std::string PrivateKeyFile = "key.pv", Signed;

      if( true == Valid )
      {
         SignMessage( "key.pv", Base64Decoded, Signed );
      }
      else
      {
         SignMessage( "key.pv", NullKey, Signed );
      }

      //////////////////////////////////////////
      //             Base64 Encode            //
      //////////////////////////////////////////
      std::string Base64EncodedSigned;
      Base64Encode( Signed, Base64EncodedSigned );

      //////////////////////////////////////////
      //             Send to Client           //
      //////////////////////////////////////////
      if( false == PipeWrite( pipe, Base64EncodedSigned ) )
         { throw( std::string( _T("Server: WriteRead returned
                                   false") ) ); }

      // If the program exits, the Client will receive
      //    ERROR_FILE_NOT_FOUND.
      ...
   }

   catch( CryptoPP::Exception& e ) {
      std::cerr << "Error: " << e.what() << std::endl;
   }

   ...

   if( NULL != pipe ) { CloseHandle( pipe ); }

   return 0;
}

Product Activation Based on RSA Signatures

Summary

Product Activation can be a useful tool for software vendors to both decrease losses due to pircay and increase their understanding of their user base. As with any technology, it should be used in moderation.

Acknowledgements

  • Wei Dai for Crypto++ and his invaluable help on the Crypto++ mailing list
  • Dr. A. Brooke Stephens who laid my Cryptographic foundations

Revisions

  • 11.24.2006 Initial Release


About the Author

Jeffrey Walton

In the past, I have worked as an IT consultant for County Government (Anne Arundel County), the Nuclear Energy Institute, the Treasury Department, and Social Security Administration as a Network Engineer and System Administrator. Primary Administration experience includes Microsoft Windows and Novell Netware, with additional exposure and familiarity with Mac and Linux OSes. Previous to the US government, I was a programmer for a small business using Microsoft Visual Languages (Basic 5.0, 6.0, and C++ 5.0, 6.0) and Scripting Languages. An undergraduate degree (BS in Computer Science) was obtained from University of Maryland, Baltimore County. Graduate work includes a Masters of Science (Computer Science) from Johns Hopkins University (expected before 2009). Training and Certifications include Microsoft, Checkpoint, and Cisco.

Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: May 6, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT While you likely have very good reasons for remaining on WinXP after end of support -- an estimated 20-30% of worldwide devices still are -- the bottom line is your security risk is now significant. In the absence of security patches, attackers will certainly turn their attention to this new opportunity. Join Lumension Vice President Paul Zimski in this one-hour webcast to discuss risk and, more importantly, 5 pragmatic risk mitigation techniques …

  • The latest release of SugarCRM's flagship product gives users new tools to build extraordinary customer relationships. Read an in-depth analysis of SugarCRM's enhanced ability to help companies execute their customer-facing initiatives from Ovum, a leading technology research firm.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds