Click here to Skip to main content
6,935,055 members and growing! (14,849 online)
Email Password   helpLost your password?
General Programming » Cryptography & Security » Cryptography     Intermediate

Product Activation Based on RSA Signatures

By Jeffrey Walton

Validate Product Keys using RSA Signing Functions and Crypto++
VC6, VC7, VC7.1, WindowsVS.NET2003, VS2005, Dev
Posted:23 Nov 2006
Updated:6 Jun 2007
Views:158,692
Bookmarked:233 times
printPrint Friendly   add Share
      Discuss Discuss   Broken Article?Report  
50 votes for this article.
Popularity: 8.18 Rating: 4.81 out of 5
3 votes, 6.0%
1

2

3
3 votes, 6.0%
4
44 votes, 88.0%
5

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++
  • Product Activation
  • Named Pipe Client/Server
  • Generating and Serializing RSA Keys
  • RSA Signing and Verification Functions
  • Product Key Signing

This article signs the Product Key, which may prove to be too restrictive to the reader. In this case, see Installation IDs Based on Truncated Hashing for an implementation of hardware fingerprints for use with Product Activations.

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. Though 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 lay 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++

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 niceties for using the Crypto++ Library.

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 developing end user demographics and thwarting piracy.

In the Piracy arena, an Activation Server can:

  • detect circulated Product Keys using statistical methods
  • negate the effects of a Key Generator
  • enforce renewal periods by removing the system time functions from the end user's computer

Detecting circulated Product Keys and Renewal Periods are basically self explanatory. However, negating a Key Generator may not be readily apparent. If a user presents a Product Key which was not created by the company (which is stored in a database of issued keys), one can assume a lucky guess or a Key Generator was used to produce the key.

Microsoft uses Product Activation 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.

Screenshot - image02.png

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 which 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 since 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 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, since 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.

        ...
    }

    catch( CryptoPP::Exception& e ) {
        std::cerr << "Error: " << e.what() << 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; }
}

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;
    }

    ...

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

    return 0;
}

bool PipeWrite( const HANDLE& pipe, const std::string& Message )
{
    DWORD dwWritten = 0;

    ...

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

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

    ...

    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);
    ...

    return( INVALID_HANDLE_VALUE != pipe );
}

void DebugMessage( const std::string& message, bool ExtraCRLF )
{
    ...
}

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 use a Symmetric Ciper such as AES; or 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()

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";

RandomPool randPool;

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();

Below is the result of running patest2 with a seed size of 0 (a slight adaptation of the Crypto++ sample code).

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 typical 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 PublicKeyFile  = "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;
}

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;
}

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

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);

// Initialize 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

// 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;
}

Hardware Fingerprinting

This article signs the Product Key, which may prove to be too restrictive to the reader. In this case, see Installation IDs Based on Truncated Hashing for an implementation of hardware fingerprints for use with Product Activations. Unique installations can be tracked by way of hashing of end user hardware components.

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 since 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 effect the system less than a change in CPUs.

Weighting

To provide tolerance, the Activation logic should allow for hardware changes (i.e., allow X components to change before invoking a reactivation).

The author proposes the following logic to allow for tolerance. It assigns a weight to each component. For example, a motherboard has a weight of 12:

  • COMM Port: 1 to 2
  • LPT Port: 1 to 2
  • USB Device (iPod, Blackberry): 2 to 3
  • Floppy Drive: 4 to 5
  • Memory: 6
  • Processor: 6
  • CD ROM: 6
  • DVD ROM: 6
  • Sound Card: 7
  • Network Adapter: 8
  • Video Card: 8
  • Hard Disk: 10
  • Motherboard: 12
  • BIOS: 12

As the software author detects changes, add the weighted value of the device. When the weighted value exceeds a threshold, require reactivation. The examples below presume a threshold of 14

  • Turn Off Comm Ports - no reactivation
  • BlackBerry Device attachment - no reactivation
  • Upgrade Memory and Processor - no reactivation
  • Upgrade Hard Disk and Memory - reactivation required
  • Upgrade Video Card and DVD ROM - activation required

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 (i.e., as a string concatenation). 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.

The following two outputs show the results of sample five. 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;

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 = false. This will cause the Server to Sign 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(), while the same is true for VerifySignature() with respect to the Client.

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;
}

Setup Programs

It is the author's opinion that user processes should be as easy as possible for the user - not the programmer. Below is the setup program from Adobe Acrobat 6.0. The Product Key is Pretty Printed. Also notice that by using one Edit control, a user can Copy and Paste a Product Key. Though this article depicts Symantec AntiVirus 2005 Setup earlier (it is more pleasing), Adobe's Setup program is easier to use for an end user.

Thomas Holte offers a MFC Extension Class CProductKey for smart pasting of Product Keys.

A Note on User Interface Design

The reader is highly encouraged to employ techniques of quality User Interface design. For example, Sybex's CCNA Virtual Lab, Platinum Edition displays the following ("Unknown Error -52" is simply not acceptable). The author is relieved he purchased the Platinum Edition, and not the Aluminium, Brass, or Silver Edition.

And an example from ACDSee 6.0. ACDSee presumably moved the script location of the validation and update routines on its servers over time.

Summary

Product Activation can be a useful tool for software vendors to both decrease losses due to piracy 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

  • 06.09.2007 Added Weighting
  • 06.09.2007 Added Reference to Installation IDs Based on Truncated Hashing
  • 05.26.2007 Added Note on Key Generators
  • 05.26.2007 Updated Broken Article Links
  • 12.27.2006 Verified Visual Studio 7.0 and 7.1 Upgrade
  • 12.14.2006 Added Checksums
  • 12.13.2006 Add User Interface Design Considerations Section
  • 12.13.2006 Add Setup Programs Section
  • 12.13.2006 Grammar and Typographical Corrections
  • 12.11.2006 Updated Article Links
  • 11.24.2006 Initial Release

Checksums

  • patest1.zip
    • MD5: CD5B65B7ACDBA247EDE28FBA169F2F26
    • SHA-1: 5751B71EFED61D91B3AD048C0C81F15397DAF434
    • SHA-256: 9AE08AA516BA5739A49A4D3E1C160BBB966248801202470F282C16841B9866F2
  • patest2.zip
    • MD5: A264675D303F15BE02F5EF0720795A59
    • SHA-1: 7029525A0D77215BD856AAB20DCB5BCB75F10700
    • SHA-256: D4D180B1C41F6534020BE5858F59F60BD5234AC74D5301FD4280134638508FF5
  • patest3.zip
    • MD5: 1D1C91C75763DA9B8EC2DFF95F35E6CA
    • SHA-1: 9D7ECC747CF136F1C8DEE4D562BA20B919F9FA55
    • SHA-256: 8349CD2FED5C290D692E428137C6E4C6D9D15339316966D2CFCE1739FB47AA68
  • patest4.zip
    • MD5: 4866F7275CDB5D3AADE861BE4F538A4D
    • SHA-1: 114A5A9DDC42B3F65A7DECCBC179B35D362227C5
    • SHA-256: 23543BDEBE5442653ABAB9662907945B5D7EDB8850FFFBDC9E14BB0C4E318A9C
  • patest5.zip
    • MD5: E39585D2491651CFEAA4A658BC20382F
    • SHA-1: CD6083024A0EAB1605C626C38D09FE774695BD23
    • SHA-256: 4D97CEF3B18C468092B3CB48AB3FEE6964FE9D8874DDA86C889890417A356939
  • patest6.zip
    • MD5: C15CCF9856F2D2385EE16B3F12F44FDB
    • SHA-1: 25C3223692851841A30645963A698B4150FC8646
    • SHA-256: 29A7F98DD5A4B066860C5EAFDF92A474BD96FCE86A7F692729C27826007D5A2D

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

Jeffrey Walton


Member
Currently I am providing services as a private consultant. In the past, I have worked as an both an IT contractor and IT consultant for County Government (Anne Arundel County, MD), the Nuclear Energy Institute, the Treasury Department, and Social Security Administration. Primary roles with the Federal Government were Network Engineer and System Administrator. Administration experience is dominated by Microsoft Windows and includes Novell NetWare, with additional exposure and familiarity with Mac and Linux OS's.

An undergraduate degree (Bachelor of Science, Computer Science) was obtained from University of Maryland. Graduate work includes a Masters of Science (Computer Science) from Johns Hopkins University (expected in the near future).

Training and Certifications include CISSP, Microsoft, Checkpoint, and Cisco.

In addition to the Networking experience, I am a principal partner in an IT adventure specializing in Tamper Sensitive and Tamper Resistant software.

In what's left of spare time I enjoy reading, spinning my Rubiks Cube, and researching the factorization of RSA Moduli (the Integer Factorization Problem).
Occupation: Systems / Hardware Administrator
Location: United States United States

Other popular Cryptography & Security articles:

Article Top
You must Sign In to use this message board.
FAQ FAQ 
 
Noise Tolerance  Layout  Per page   
 Msgs 1 to 25 of 32 (Total in Forum: 32) (Refresh)FirstPrevNext
GeneralValidating keys with no internet connection available. Pinmemberkavitesh4:00 24 Sep '09  
GeneralSerial number generation from RSA... PinmemberBen Lesh5:15 13 Dec '07  
GeneralRe: Serial number generation from RSA... PinmemberPaul Sanders (AlpineSoft)5:31 13 Dec '07  
GeneralRe: Serial number generation from RSA... PinmemberBen Lesh9:57 13 Dec '07  
GeneralIt's the only way to stop software piracy [modified] PinmemberPaul Sanders (AlpineSoft)4:43 21 Oct '07  
GeneralRe: It's the only way to stop software piracy PinmemberBen Lesh5:18 13 Dec '07  
GeneralEncrypt a string, Key not in file PinmemberErnestoNet7:10 28 Jun '07  
GeneralRe: Encrypt a string, Key not in file PinmemberJeffrey Walton14:59 30 Jun '07  
Hi Ernesto,

Use the various Set... methodes of InvertibleRSAFunction class. See InvertibleRSAFunction[^] Documentation.

Jeff
GeneralExcellent Work PinmemberDeborah Sampson3:04 21 Jun '07  
GeneralRe: Excellent Work PinmemberJeffrey Walton4:08 21 Jun '07  
GeneralCommercial product activation PinmemberV.S.13:19 28 May '07  
GeneralRe: Commercial product activation PinmemberJeffrey Walton13:48 28 May '07  
GeneralRe: Commercial product activation PinmemberJeffrey Walton20:03 10 Jun '07  
GeneralVery thurough PinmemberJames Ashley9:07 27 May '07  
GeneralRe: Very thurough PinmemberJeffrey Walton9:48 27 May '07  
GeneralRe: Very thurough PinmemberJeffrey Walton5:56 28 May '07  
GeneralHow to use with c# Pinmembererman.olca2:46 3 Jan '07  
GeneralRe: How to use with c# PinmemberJeffrey Walton3:52 9 Jan '07  
GeneralRe: How to use with c# PinmemberJeffrey Walton8:39 28 May '07  
GeneralAmusing Pinmemberncarey8:59 27 Nov '06  
GeneralRe: Amusing PinmemberJeffrey Walton10:04 27 Nov '06  
GeneralGood Article PinmemberNeville Franks10:09 24 Nov '06  
GeneralRe: Good Article [modified] PinmemberJeffrey Walton13:50 24 Nov '06  
GeneralRe: Good Article PinmemberGarth J Lancaster14:37 24 Nov '06  
GeneralRe: Good Article PinmemberJeffrey Walton8:31 28 May '07  

General General    News News    Question Question    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+PgUp/PgDown to switch pages.

PermaLink | Privacy | Terms of Use
Last Updated: 6 Jun 2007
Editor: Deeksha Shenoy
Copyright 2006 by Jeffrey Walton
Everything else Copyright © CodeProject, 1999-2010
Web19 | Advertise on the Code Project