Premessina
Come programmatori dotnet, ci troviamo a volte ad affrontare il problema di proteggere le nostre applicazioni da occhi indiscreti che potrebbero curiosare troppo nel nostro codice.
Il problema che mi sono trovato ad affrontare più volte è quello di proteggere il codice che gestisce la licenza che concedo per le mie applicazioni. Una soluzione potrebbe essere quella di offuscare il codice, oppure potrei appoggiarmi a codice unmanaged, tuttavia sono soluzioni che non mi hanno mai attirato più di tanto e che di fatto non garantiscono un elevato grado di protezione a sguardi esperti.
La soluzione che mi è venuta in mente è ben diversa dalle soluzioni che "nascondono" il codice per fare sicurezza, al contrario la mia soluzione prevede assoluta trasparenza. Ed è anche il motivo per cui scrivo questo post. Spero che possiate aiutarmi a trovare le falle e rendere ancora più sicuro il tutto.
Come fare (la teoria)
Visto che nascondere il codice o criptare licenza e altro è una strada che ho scartato, la cosa più naturare è scrivere il codice di licenza, con tutte le informazioni che sono ad esso legate (come ad esempio:data di attivazione, data di scadenza, numero di utenti, licenziatario, productkey e altro), all interno di un file xml nel path di installazione. (Evitiamo allo smanettone di turno la fatica di doversi cercare il file di licenza)
Lo scriviamo anche il chiaro in modo da poterlo consultare tranquillamente senza dover ricorrere a tools particolari.
Inseriamo all interno del file stesso un tag che identifica in modo univoco la macchina per la quale la licenza è stata rilasciata (vedremo in seguito come ottenere queste informazioni)
Nella nostra applicazione quindi quello che dobbiamo fare è leggere il file di licenza, fare le verifiche del caso per accertarci che il productkey sia valido e che le informazioni all interno della licenza siano corrette, verificare il codice che lega la licenza alla macchina ed eseguire la nostra applicazione.
Rimane un problema: evitare che qualcuno modifichi a piacimento il file di licenza.
La soluzione viene quasi da sola. E sufficiente firmare il file XML con una chiave RSA. Chiunque modificherà il file di licenza, corromperà la firma e il file risulterà non più valido.
Abbiamo sempre a disposizione una chiave RSA che viene gia distribuita all interno dei nostri assembly: la stessa chiave che utilizzamo per firmare i nostri assembly.
Viene generata da VisualStudio, e ne viene distribuita solo la parte pubblica insieme alla nostra applicazione, la parte privata rimane di nostra esclusiva proprietà.
Quindi riassumendo quello che dobbiamo fare è:
- Creare un file di licenza in fomato XML
- Generare un file snk (che contiene sia chiave pubblica che privata)
- Firmare i nostri assembly con il file snk
- Firmare il file di licenza con il file snk
- Distribuire applicazione e file di licenza
- Prima di eseguire l applicazione verificare che la firma del file di licenza sia ancora valida.
Come fare (la pratica)
Per prima cosa ci serve una struttura per scrivere il file di licenza, quindi creiamo una classe serializzabile per contenere tutte quest informazioni.
[code language="c#"]
[Serializable]
internal class BaseLicense
{
private string _name;
private object _value;
public object Value
{
get { return _value; }
set { _value = value; }
}
public virtual string ObjectName
{
get { return _name; }
set { _name = value; }
}
}
[Serializable]
internal class PCInfo : BaseLicense
{
private string _signature;
public string PCSignature
{
get { return _signature; }
set { _signature = value; }
}
}
[Serializable]
internal class LicenseArchive
{
private Dictionary<string, BaseLicense> _licenses = new Dictionary<string, BaseLicense>();
private byte[] _signature = new byte[0];
public byte[] Signature
{
get { return _signature; }
set { _signature = value; }
}
public Dictionary<string, BaseLicense> Licenses
{
get { return _licenses; }
set { _licenses = value; }
}
}
[/code]
La classe LicenseArchive è il nostro file di licenza, contiene un dictionary e una proprietà pubblica "Signature" che conterrà la firma del file xml stesso. Il dictionary esposto con la proprietà Licenses conterrà tutte le informazioni della licenza.
La classe LicenseBase è un contenitore che contiene una coppia di informazioni "Chiave-Valore" della licenza. La classe PCInfo arricchisce queste informazioni con una stringa che contiene l identificativo del PC per il quale è stata rilasciata la licenza.
Vediamo come leggere le informazioni per identificare in modo univoco il PC.
Possiamo utilizzare WMI (Windows Management Instrumentation) per ottenere le informazioni che ci servono.
[code language="c#"]
internal static class MachineInfo
{
/// <summary>
/// return Volume Serial Number from hard drive
/// </summary>
/// <param name="strDriveLetter">[optional] Drive letter</param>
/// <returns>[string] VolumeSerialNumber</returns>
public static string GetVolumeSerial(string strDriveLetter)
{
if (strDriveLetter == "" || strDriveLetter == null) strDriveLetter = "C";
ManagementObject disk =
new ManagementObject("win32_logicaldisk.deviceid=\"" + strDriveLetter + ":\"");
disk.Get();
return disk["VolumeSerialNumber"].ToString();
}
/// <summary>
/// Returns MAC Address from first Network Card in Computer
/// </summary>
/// <returns>[string] MAC Address</returns>
public static string GetMACAddress()
{
ManagementClass mc = new ManagementClass("Win32_NetworkAdapterConfiguration");
ManagementObjectCollection moc = mc.GetInstances();
string MACAddress = String.Empty;
foreach (ManagementObject mo in moc)
{
if (MACAddress == String.Empty) // only return MAC Address from first card
{
if ((bool)mo["IPEnabled"] == true) MACAddress = mo["MacAddress"].ToString();
}
mo.Dispose();
}
MACAddress = MACAddress.Replace(":", "");
return MACAddress;
}
/// <summary>
/// Return processorId from first CPU in machine
/// </summary>
/// <returns>[string] ProcessorId</returns>
public static string GetCPUId()
{
string cpuInfo = String.Empty;
string temp = String.Empty;
ManagementClass mc = new ManagementClass("Win32_Processor");
ManagementObjectCollection moc = mc.GetInstances();
foreach (ManagementObject mo in moc)
{
if (cpuInfo == String.Empty)
{// only return cpuInfo from first CPU
cpuInfo = mo.Properties["ProcessorId"].Value.ToString();
}
}
return cpuInfo;
}
}
[/code]
Ci siamo, abbiamo tutto il necessario per generare il nostro file di licenza.
Vediamo cosa ci serve per firmarlo.
Ho trovato questa classettina che ci permette di ottenere le chiavi RSA da un file snk e dalla parte dell snk che si trova all interno dell assembly.
[code language="C#"]
///
/// One static method to get an RSACryptoServiceProvider from a *.snk file.
/// NOTE: These methods assume 1024 bit keys, the same as exported from sn.exe.
/// See also: CryptExportKey() Win32 API.
///
public sealed class SnkUtil
{
private const int magic_priv_idx = 0x08;
private const int magic_pub_idx = 0x14;
private const int magic_size = 4;
private SnkUtil()
{
}
///
/// Returns RSA object from *.snk key file.
///
/// Path to snk file.
/// RSACryptoServiceProvider
public static RSACryptoServiceProvider GetRSAFromSnkFile(string path)
{
if (path == null)
throw new ArgumentNullException("path");
byte[] snkBytes = GetFileBytes(path);
if (snkBytes == null)
throw new Exception("Invalid SNK file.");
RSACryptoServiceProvider rsa = GetRSAFromSnkBytes(snkBytes);
return rsa;
}
private static byte[] GetFileBytes(string path)
{
using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read))
using (BinaryReader br = new BinaryReader(fs))
{
byte[] bytes = br.ReadBytes((int)fs.Length);
return bytes;
}
}
public static RSACryptoServiceProvider GetRSAFromSnkBytes(byte[] snkBytes)
{
if (snkBytes == null)
throw new ArgumentNullException("snkBytes");
RSAParameters param = FigureParams(snkBytes);
// Must set KeyNumber to AT_SIGNATURE for strong
// name keypair to be correctly imported.
CspParameters cp = new CspParameters();
cp.KeyNumber = 2; // AT_SIGNATURE
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(1024, cp);
rsa.ImportParameters(param);
return rsa;
}
private static byte[] BlockCopy(byte[] source, int idx, int size)
{
if ((source == null) || (source.Length < (idx + size)))
return null;
byte[] ret = new byte[size];
Buffer.BlockCopy(source, idx, ret, 0, size);
return ret;
}
///
/// Returns true if buffer length is public key size.
///
///
///
private static bool SnkBufIsPubLength(byte[] keypair)
{
if (keypair == null)
return false;
return (keypair.Length == 160);
}
///
/// Check that RSA1 is in header (public key only).
///
///
///
private static bool CheckRSA1(byte[] pubkey)
{
// Check that RSA1 is in header.
// R S A 1
byte[] check = new byte[] { 0x52, 0x53, 0x41, 0x31 };
return CheckMagic(pubkey, check, magic_pub_idx);
}
///
/// Check that RSA2 is in header (public and private key).
///
///
///
private static bool CheckRSA2(byte[] pubkey)
{
// Check that RSA2 is in header.
// R S A 2
byte[] check = new byte[] { 0x52, 0x53, 0x41, 0x32 };
return CheckMagic(pubkey, check, magic_priv_idx);
}
private static bool CheckMagic(byte[] keypair, byte[] check, int idx)
{
byte[] magic = BlockCopy(keypair, idx, magic_size);
if (magic == null)
return false;
for (int i = 0; i < magic_size; i++)
{
if (check
!= magic
)
return false;
}
return true;
}
///
/// Returns RSAParameters from byte[].
///
///
///
private static RSAParameters FigureParams(byte[] keypair)
{
RSAParameters ret = new RSAParameters();
if ((keypair == null) || (keypair.Length < 1))
return ret;
bool pubonly = SnkBufIsPubLength(keypair);
if ((pubonly) && (!CheckRSA1(keypair)))
return ret;
if ((!pubonly) && (!CheckRSA2(keypair)))
return ret;
int magic_idx = pubonly ? magic_pub_idx : magic_priv_idx;
// Bitlen is stored here, but note this
// class is only set up for 1024 bit length keys
int bitlen_idx = magic_idx + magic_size;
int bitlen_size = 4; // DWORD
// Exponent
// In read file, will usually be { 1, 0, 1, 0 } or 65537
int exp_idx = bitlen_idx + bitlen_size;
int exp_size = 4;
//BYTE modulus[rsapubkey.bitlen/8]; == MOD; Size 128
int mod_idx = exp_idx + exp_size;
int mod_size = 128;
//BYTE prime1[rsapubkey.bitlen/16]; == P; Size 64
int p_idx = mod_idx + mod_size;
int p_size = 64;
//BYTE prime2[rsapubkey.bitlen/16]; == Q; Size 64
int q_idx = p_idx + p_size;
int q_size = 64;
//BYTE exponent1[rsapubkey.bitlen/16]; == DP; Size 64
int dp_idx = q_idx + q_size;
int dp_size = 64;
//BYTE exponent2[rsapubkey.bitlen/16]; == DQ; Size 64
int dq_idx = dp_idx + dp_size;
int dq_size = 64;
//BYTE coefficient[rsapubkey.bitlen/16]; == InverseQ; Size 64
int invq_idx = dq_idx + dq_size;
int invq_size = 64;
//BYTE privateExponent[rsapubkey.bitlen/8]; == D; Size 128
int d_idx = invq_idx + invq_size;
int d_size = 128;
// Figure public params
// Must reverse order (little vs. big endian issue)
ret.Exponent = BlockCopy(keypair, exp_idx, exp_size);
Array.Reverse(ret.Exponent);
ret.Modulus = BlockCopy(keypair, mod_idx, mod_size);
Array.Reverse(ret.Modulus);
if (pubonly) return ret;
// Figure private params
// Must reverse order (little vs. big endian issue)
ret.P = BlockCopy(keypair, p_idx, p_size);
Array.Reverse(ret.P);
ret.Q = BlockCopy(keypair, q_idx, q_size);
Array.Reverse(ret.Q);
ret.DP = BlockCopy(keypair, dp_idx, dp_size);
Array.Reverse(ret.DP);
ret.DQ = BlockCopy(keypair, dq_idx, dq_size);
Array.Reverse(ret.DQ);
ret.InverseQ = BlockCopy(keypair, invq_idx, invq_size);
Array.Reverse(ret.InverseQ);
ret.D = BlockCopy(keypair, d_idx, d_size);
Array.Reverse(ret.D);
return ret;
}
}
[/code]
Quando avremo ottenuto le chiavi RSA dovremo semplicemente passarle ad un Crypto service provider per fare la firma.
Daremo in pasto al metodo SignData di RSACryptoServiceProvider i dati xml da firmare.
In questo caso utilizzeremo il file snk che abbiamo generato per firmare il nostro assembly.
[code language="C#"]
RSACryptoServiceProvider rsa = SnkUtil.GetRSAFromSnkFile(snkfilepath);
byte[] signature;
signature = rsa.SignData(datatoSign, new SHA1CryptoServiceProvider());
[/code]
Per verificare la firma abbiamo solo bisogno della chiave pubblica e non dell intero snk. possiamo ottenere la chiave pubblica direttamente dall assembly.
[code language="C#"]
byte[] publickey = Assembly.GetExecutingAssembly().GetName().GetPublicKey();
if (publickey == null || publickey.Length == 0)
throw new LicenseException(null, null, "This assembly cannot be licensed because is not strongly named");
RSACryptoServiceProvider rsa = SnkUtil.GetRSAFromSnkBytes(publickey);
return rsa.VerifyData(datatoverify, new SHA1CryptoServiceProvider(), signature);
[/code]
Questo è tutto. Vi riporto per intero anche le classi che ho scritto per serializzare e firmare la licenza ( da utilizzare nel nostro license manager), e la classe LicenseReader che inseriremo all interno del nostro client.
[code language="c#"]
internal class LicenseReader
{
private LicenseArchive _licenses = new LicenseArchive();
protected LicenseArchive LicensesArchive
{
get { return _licenses; }
}
public LicenseReader()
{
this.LoadLicenses();
}
private bool VerifyObjectSign(byte[] datatoverify, byte[] signature)
{
byte[] publickey = Assembly.GetExecutingAssembly().GetName().GetPublicKey();
if (publickey == null || publickey.Length == 0)
throw new LicenseException(null, null, "This assembly cannot be licensed because is not strongly named");
RSACryptoServiceProvider rsa = SnkUtil.GetRSAFromSnkBytes(publickey);
return rsa.VerifyData(datatoverify, new SHA1CryptoServiceProvider(), signature);
}
protected void LoadLicenses()
{
BinaryFormatter bf = new BinaryFormatter();
// Get license file path
string licensepath = Application.StartupPath + @"\" + "TeamDev.Licenses.lic";
//Search for License File
if (!File.Exists(licensepath)) return;
FileStream fs = File.OpenRead(licensepath);
// Deserialize the license file
_licenses = (LicenseArchive)bf.Deserialize(fs);
fs.Close();
// serialize in a bytestream the Licenses for validation purposes
MemoryStream ms = new MemoryStream();
bf.Serialize(ms, _licenses.Licenses);
byte[] buffer = new byte[ms.Length];
ms.Position = 0;
ms.Read(buffer, 0, (int)ms.Length);
// Check the PC Signature & LicenseFile Signature.
try
{
string pcinfo = string.Empty;
try { pcinfo += MachineInfo.GetCPUId(); }
catch { }
try { pcinfo += MachineInfo.GetVolumeSerial("C"); }
catch { }
// Il PCInfo non può essere vuoto.
if (pcinfo == string.Empty)
throw new Exception("Invalid License File");
if ((((PCInfo)_licenses.Licenses["PCINFO"]).PCSignature != pcinfo) ||
(!VerifyObjectSign(buffer, _licenses.Signature)))
{
_licenses.Licenses.Clear();
_licenses.Signature = new byte[0];
throw new Exception("Invalid License File");
}
}
catch (Exception ex)
{
throw new Exception("Invalid License File", ex);
}
}
}
internal class LicenseProvider : LicenseReader
{
private string _snkFilePath = string.Empty;
public LicenseArchive Archive
{
get { return base.LicensesArchive; }
}
public string SnkFilePath
{
get { return _snkFilePath; }
set { _snkFilePath = value; }
}
// Override Default Constructor because i don't like to load license
// at startup.
public LicenseProvider()
{
this.ClearLicenses();
}
public void ClearLicenses()
{
base.LicensesArchive.Licenses.Clear();
base.LicensesArchive.Signature = new byte[0];
}
public void SaveLicenseFile(string filename)
{
BinaryFormatter bf = new BinaryFormatter();