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.
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.
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") ) );}
...
}
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 );
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 );
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.
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 )
{
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:
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;
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;
RSAES_OAEP_SHA_Decryptor Decryptor( rng, 512 );
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 |
#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;
CryptoPP::RSAES_OAEP_SHA_Decryptor Decryptor( rng, 512 );
CryptoPP::HexEncoder privFile(new
CryptoPP::FileSink( PrivateKeyFile.c_str() )
);
Decryptor.DEREncode(privFile);
privFile.MessageEnd();
CryptoPP::RSAES_OAEP_SHA_Encryptor Encryptor(Decryptor);
CryptoPP::HexEncoder pubFile(new
CryptoPP::FileSink( PublicKeyFile.c_str() )
);
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.
#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.
AutoSeededRandomPool rng;
std::string message = "X3BA-9NSF-8N9Q-UWQC-U7FX-AZZF-JAJW";
std::string SignedFile = "message.sig";
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() )
) ) );
Verification is a bit more complicated, but not insurmountable by any means. Again, the Message M is in memory rather than on disk.
std::string PublicKeyFile = "key.pb";
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;
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() )
) ) );
#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
{
AutoSeededRandomPool rng;
std::string message = "X3BA-9NSF-8N9Q-UWQC-U7FX-AZZF-JAJW";
std::string SignedFile = "message.sig";
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() )
) ) );
std::string PublicKeyFile = "key.pb";
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 |
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 )
) );
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 )
) );
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") ) ); }
std::string Base64Decoded;
Base64Decode( Received, Base64Decoded );
bool Valid = true;
std::string PrivateKeyFile = "key.pv", Signed;
if( true == Valid )
{
SignMessage( "key.pv", Base64Decoded, Signed );
}
else
{
SignMessage( "key.pv", NullKey, Signed );
}
std::string Base64EncodedSigned;
Base64Encode( Signed, Base64EncodedSigned );
if( false == PipeWrite( pipe, Base64EncodedSigned ) )
{ throw( std::string( _T("Server: WriteRead returned false") ) );}
...
}
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