Serial Key Generation in .NET: From Simple Keys to Cryptographic Licensing
A serial key is the mechanism that separates a paying customer from someone running your software without authorization. The implementation quality of that mechanism determines whether your licensing holds up against casual sharing, automated key generators, or targeted reverse engineering.
Most developers building their first licensing system start with a mental model shaped by entering product keys themselves: a formatted string of characters that the application accepts or rejects. What that model does not reveal is the spectrum of security properties a key scheme can have, from a trivially reversible format check to a cryptographic commitment that an attacker cannot forge even after fully decompiling the validation code.
Understanding that spectrum is the first step toward choosing the right approach for your application's commercial value and threat model.
Structured keys with checksums
The most famous example of this approach is the Windows 95 product key. The key format was XXX-XXXXXXX, and the validation was pure arithmetic: the sum of the seven digits in the second part had to be divisible by 7. No cryptography, no signatures - just (d1 + d2 + d3 + d4 + d5 + d6 + d7) % 7 == 0. Anyone could write a key generator in minutes. The "legendary" key 111-1111111 passed validation trivially. The algorithm leaked almost immediately and became public knowledge.
The general pattern is the same in any checksum-based scheme: embed a checksum in the key itself. Generate a key by combining a payload with a checksum computed over that payload. Validation recomputes the checksum and compares.
// Simplified example - Luhn-style checksum validation
bool IsKeyValid(string key)
{
var payload = key.Substring(0, key.Length - 1);
var checkDigit = key[^1];
return ComputeCheckDigit(payload) == checkDigit;
}This prevents typos and casual guessing - a random string fails validation with high probability. It is trivially defeated by anyone who decompiles the validation and reads the checksum algorithm. Within minutes, an attacker can write a key generator that produces unlimited valid keys.
Appropriate for: low-value software where casual piracy prevention is sufficient and a key generator circulating is not a serious business threat.
Hardcoded key lists: a tempting shortcut
Another common first approach is to skip validation logic entirely and just store a list of valid keys - or their hashes - directly in the binary. Back in 2004, when I built my first shareware program (TrafficSpeedViewer, a plugin for Internet Explorer), this is exactly what I did: generated a couple hundred short keys, computed their hashes, and hardcoded the hash list in the application. Validation was a simple lookup - hash the entered key and check whether it appears in the list.
It felt safer than a checksum because the original keys were not in the binary. But the hash list itself is extractable. An attacker who decompiles the application has all the hashes and can brute-force short keys against them. The key pool is also finite - once you sell more licenses than you generated keys, you need a new approach entirely. This scheme does not scale and does not resist even basic reverse engineering.
HMAC-based keys
A stronger approach computes an HMAC over a structured payload using a secret key. The payload can encode customer information, feature flags, or expiration dates. Validation recomputes the HMAC and compares.
// Simplified example - HMAC-based key validation
bool ValidateKey(string serialKey)
{
var parts = DecodeKey(serialKey);
var payload = parts.Payload; // customer ID, edition, expiry
var signature = parts.Signature;
using var hmac = new HMACSHA256(embeddedSecret);
var expected = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
return CryptographicOperations.FixedTimeEquals(expected, signature);
}An attacker who decompiles the validation code learns the algorithm but not the secret - they cannot generate new valid keys without it. The weakness: the HMAC secret must be embedded somewhere in the binary for the validation to work. An attacker who extracts it can generate unlimited valid keys that pass the unmodified validation.
The security of this scheme depends entirely on how well the embedded secret is hidden. String encryption prevents the secret from being a readable literal in the binary. Code virtualization on the validation method makes it harder to trace where the secret is used. But the fundamental limitation remains: the secret is in the binary, and a sufficiently determined attacker can find it.
Appropriate for: commercial software with a real revenue concern, where the validation secret is protected by obfuscation and the threat model is opportunistic rather than targeted.
Asymmetric signature-based keys
The strongest scheme uses asymmetric cryptography. Keys are generated by signing a payload with a private key (RSA, ECDSA). Validation uses only the corresponding public key. The private key never appears in the distributed binary - it lives on the generation server or in a key generation tool that the developer controls.
// Simplified example - asymmetric key validation
bool ValidateKey(string serialKey)
{
var parts = DecodeKey(serialKey);
var payload = parts.Payload;
var signature = parts.Signature;
using var rsa = RSA.Create();
rsa.ImportRSAPublicKey(embeddedPublicKey, out _);
return rsa.VerifyData(
Encoding.UTF8.GetBytes(payload),
signature,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
}An attacker who fully decompiles the validation code finds the public key, which is useless for generating new keys - only the private key can produce valid signatures. The attacker cannot forge keys. They can only distribute specific valid keys they already possess, or patch the validation code to bypass the signature check entirely.
This changes the attack from secret extraction (the HMAC weakness) to validation bypass. The attacker does not need to forge keys - they need to find the validation method and modify it to always return true. The defense shifts from hiding a secret to making the validation logic itself resistant to modification: code virtualization on the validation method, anti-tamper detection, and control flow obfuscation that makes the return path untraceable.
Appropriate for: software with significant commercial value where key generation security is a genuine priority.
The consistent theme across all three schemes
At each level, obfuscation is the mechanism that makes the scheme resilient against its primary attack:
| Scheme | Primary attack | How obfuscation helps |
|---|---|---|
| Checksum | Attacker reads the validation algorithm | Obfuscation makes the algorithm harder to read. Weak defense for a weak scheme. |
| HMAC | Attacker extracts the embedded secret | String encryption and code virtualization hide the secret. Meaningful defense that extends the scheme's practical lifespan. |
| Asymmetric | Attacker patches the validation to skip the signature check | Code virtualization makes the validation method resistant to modification. Strong defense that matches the cryptographic scheme's strength. |
The key scheme and the code protection are not separate decisions. They are two parts of the same design.
What a production key system still requires beyond key validation
A working key validation algorithm is one component. A production licensing system also requires:
A key payload format that encodes the information your business needs: customer name, email, edition, feature flags, expiration date, hardware ID. The payload must be structured, versioned, and extensible as your licensing model evolves.
A key generation tool or server that lives separately from the distributed binary. For asymmetric schemes the private key must never be on the same machine as the application. For HMAC schemes the secret must be in as few places as possible. Most ISVs need both a manual key generation tool (for support and sales) and a server-side generator (for automated purchase flows).
Key revocation for customers who request refunds, violate terms, or whose keys have been leaked publicly.
Expiration and subscription support if your business model uses time-limited licenses or annual subscriptions rather than perpetual licenses.
Tamper resistance for the validation logic and the license file storage. A license file that an attacker can edit, a validation method that an attacker can patch - both defeat the key scheme regardless of its cryptographic strength.
ArmDot's built-in licensing
ArmDot implements the asymmetric approach: RSA encryption for license key generation and validation. The private exponent stays in the .armdotproj project file and is used only for key generation. The public key (modulus) is embedded in the protected binary during the obfuscation process. An attacker who decompiles the application cannot forge keys.
Keys are generated via ArmDot UI, ArmDotConsole, or programmatically through key generator in PHP and key generator in C for server-side automation. The .NET API (ArmDot.Api.Keys) supports programmatic key generation from any .NET application.
Each key can embed: registration name, email, expiration date, maximum build date (keys stop working for builds after this date), hardware ID for machine locking, and arbitrary user data as bytes for custom fields like edition or feature flags.
Validating a key in the application:
[ArmDot.Client.VirtualizeCode]
private void CheckLicense(string key)
{
ArmDot.Client.Api.PutKey(key);
switch (ArmDot.Client.Api.GetLicenseState())
{
case ArmDot.Client.Api.LicenseKeyState.Valid:
string userName = ArmDot.Client.Api.GetUserName();
byte[] userData = ArmDot.Client.Api.GetUserData();
// Key valid - read payload fields
break;
case ArmDot.Client.Api.LicenseKeyState.Expired:
// Key expired
break;
case ArmDot.Client.Api.LicenseKeyState.BadHardwareId:
// Hardware mismatch
break;
case ArmDot.Client.Api.LicenseKeyState.Blocked:
// Key revoked
break;
case ArmDot.Client.Api.LicenseKeyState.Invalid:
default:
// Invalid key
break;
}
}The [VirtualizeCode] attribute converts the validation method to custom bytecode. An attacker cannot patch the signature check because the method body is no longer CIL - it is a VM interpreter loop with an opaque bytecode buffer unique to this method and this build.
Keys are base64 strings. The format is not human-friendly (XXXXX-XXXXX style) but carries more payload securely than formatted key schemes. For applications that need a shorter, formatted key for manual entry, the base64 key can be stored in a license file that the user imports rather than types.
The key generation infrastructure, the payload format, the cryptographic scheme, the expiration and revocation support, and the tamper-resistant validation are all handled. The developer's implementation work is limited to calling PutKey and GetLicenseState and deciding what the application does for each state.
Related: Hardware ID Locking in .NET → - binding a license key to a specific machine.
Related: Code Virtualization for .NET → - the technique that protects the validation logic from being patched out.
Back to: .NET Licensing Protection →
ArmDot licensing
ArmDot provides RSA-based serial key generation and validation alongside its obfuscation capabilities. Key generators available in PHP and C for server-side automation. The licensing API is part of the same ArmDot.Client NuGet package. Free trial available - protected assemblies stop working after two weeks.
