Click here to Skip to main content
Click here to Skip to main content

Product Activation Based on RSA Signatures

By , 6 Jun 2007
Rate this:
Please Sign up or sign in to vote.

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 c***s (Chief Mathematician at GCHQ - the British equivalent of the NSA) described the system in 1973. However, c***s 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
Systems / Hardware Administrator
United States United States
No Biography provided

Comments and Discussions

 
QuestionGreat article but i have a problem with signer filter. Pinmemberpsychegr10-Mar-14 14:59 
Questioncompiling problems PinmemberCool Smith12-Feb-12 3:31 
Questionself decryptor program Pinmemberyasihunter8-Apr-11 19:42 
QuestionVery good explanation of product activation, however it's easy to bypass... So how can you make it harder? Pinmembermyst4ever3-Aug-10 22:56 
AnswerRe: Very good explanation of product activation, however it's easy to bypass... So how can you make it harder? PinmemberCool Smith11-Feb-12 11:56 
General[SOLVED] Can't compile cryptopp in Visual Studio 2010 Pinmemberhjkhjghjk18-May-10 9:54 
GeneralValidating keys with no internet connection available. Pinmemberkavitesh24-Sep-09 3:00 
GeneralSerial number generation from RSA... PinmemberBen Lesh13-Dec-07 4:15 
GeneralRe: Serial number generation from RSA... PinmemberPaul Sanders (AlpineSoft)13-Dec-07 4:31 
GeneralRe: Serial number generation from RSA... PinmemberBen Lesh13-Dec-07 8:57 
GeneralIt's the only way to stop software piracy [modified] PinmemberPaul Sanders (AlpineSoft)21-Oct-07 3:43 
GeneralRe: It's the only way to stop software piracy PinmemberBen Lesh13-Dec-07 4:18 
GeneralEncrypt a string, Key not in file PinmemberErnestoNet28-Jun-07 6:10 
GeneralRe: Encrypt a string, Key not in file PinmemberJeffrey Walton30-Jun-07 13:59 
GeneralExcellent Work PinmemberDeborah Sampson21-Jun-07 2:04 
GeneralRe: Excellent Work PinmemberJeffrey Walton21-Jun-07 3:08 
GeneralCommercial product activation PinmemberV.S.28-May-07 12:19 
GeneralRe: Commercial product activation PinmemberJeffrey Walton28-May-07 12:48 
GeneralRe: Commercial product activation PinmemberJeffrey Walton10-Jun-07 19:03 
GeneralVery thurough PinmemberJames Ashley27-May-07 8:07 
GeneralRe: Very thurough PinmemberJeffrey Walton27-May-07 8:48 
GeneralRe: Very thurough PinmemberJeffrey Walton28-May-07 4:56 
QuestionHow to use with c# Pinmembererman.olca3-Jan-07 1:46 
AnswerRe: How to use with c# PinmemberJeffrey Walton9-Jan-07 2:52 
AnswerRe: How to use with c# PinmemberJeffrey Walton28-May-07 7:39 
GeneralAmusing Pinmemberncarey27-Nov-06 7:59 
GeneralRe: Amusing PinmemberJeffrey Walton27-Nov-06 9:04 
GeneralGood Article PinmemberNeville Franks24-Nov-06 9:09 
GeneralRe: Good Article [modified] PinmemberJeffrey Walton24-Nov-06 12:50 
GeneralRe: Good Article PinmemberGarth J Lancaster24-Nov-06 13:37 
GeneralRe: Good Article PinmemberJeffrey Walton28-May-07 7:31 
GeneralRe: Good Article PinmemberGarth J Lancaster28-May-07 11:45 
GeneralRe: Good Article PinmemberJeffrey Walton28-May-07 12:01 
GeneralRe: Good Article PinmemberWes Aday25-Nov-06 9:30 
GeneralRe: Good Article PinmemberJeffrey Walton28-May-07 7:39 
GeneralGood job Pinmemberhamo200824-Nov-06 3:53 
GeneralRe: Good job PinmemberJeffrey Walton24-Nov-06 6:55 
GeneralRe: Good job PinmemberJeffrey Walton28-May-07 7:32 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

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

| Advertise | Privacy | Mobile
Web02 | 2.8.140415.2 | Last Updated 7 Jun 2007
Article Copyright 2006 by Jeffrey Walton
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid