In questo articolo approfondiremo i seguenti argomenti: come firmare un Assembly, cosa accade quando un Assembly viene firmato, come e perché usare Assembly firmati, come gestire un ambiente in cui non tutti hanno accesso alle chiavi per la firma digitale, come utilizzare questo tipo di modalità di lavoro, come far convivere gli obfuscator e gli Assembly firmati.
Introduzione:
Questo articolo prende spunto alcune letture che ho trovato interessanti ed illuminanti su un argomento che considero di fondamentale importanza per chi sviluppa software professionalmente. Spero, per quanto possibile, di aiutare chi ha iniziato a lavorare a progetti che vanno oltre lo studio di .NET, a capirne l’uso e l’importanza.
Gli argomenti che introdurremo sono i seguenti:
- Cos’è lo strong name.
- Come si assegna lo strong name ad un Assembly.
- Cosa succede se perdiamo la coppia di chiavi crittografiche.
- Come il compilatore costruisce lo Strong Name.
- Firmare gli Assembly a cosa serve? Ma perché farlo?
- Cosa accade se una azienda non vuole dare accesso alle chiavi private a tutti i programmatori?
- Qual’è il motivo per cui offuscare un Assembly crea dei problemi se questo è firmato?
- Conclusioni
Che cos’è lo strong name
La sua traduzione italiana letterale non ha molto senso, Nome Forte infatti non rende l’idea di ciò che questa definizione significa, la traduzione più adatta è, per quanto mi riguarda, Nome Univoco infatti, lo “Strong Naming” è l’azione di assegnare ad un Assembly .NET un nome che sia univoco fra tutti gli Assembly prodotti ovunque. Assegnare uno strong name, equivale a Firmare digitalmente il file, così come si farebbe per un documento elettronico.
Il fatto che un Assembly abbia un Nome Univoco, ci protegge (in parte) da quello che in COM era chiamato DLL HELL ovvero l’inferno delle DLL, dove l’installazione di una DLL con lo stesso nome di un’altra in una cartella di sistema poteva provocare disastri.
Lo strong name, fa in modo che un Assembly .NET, abbia un nome univoco che lo identifica, inoltre, consente al Framework di effettuare un controllo sull’Assembly stesso per verificare che non sia stato manipolato (ad esempio da un virus).
Come si assegna lo strong name ad un Assembly
Per assegnare ad un Assembly uno strong name, abbiamo per prima cosa bisogno di una coppia di chiavi crittografiche, a 1024 bit, queste due chiavi, una Privata ed una Pubblica, saranno utilizzate per apporre la firma digitale e generare il nome univoco per l’Assembly.
Per produrre una coppia di chiavi, da utilizzare per firmare digitalmente gli Assembly, il .NET Framework SDK, scaricabile gratuitamente sul sito Microsoft, fornisce, oltre ad una ricchissima collezione di documentazione, esempi, utility, e gadget per la costruzione di programmi basati su .NET Framework 2.0, anche l’utility sn.exe, (strong name utility) che permette di generare la coppia di chiavi crittografiche necessaria alla firma di un Assembly.
Per generare una coppia di chiavi crittografiche utilizzando sn.exe basta eseguire dalla console il seguente codice:
sn.exe -k DotNetWorkKeys.snk
Viene generato un file snk contenente la coppia di chiavi crittografiche a 1024 bit.
E’ possibile estrarre dal file snk la sola chiave pubblica generata utilizzando il comando:
sn.exe -p DotNetWorkKeys.snk DotNetWorkPublicKey.snk
Per chi utilizza Visual Studio 2005, la coppia di chiavi per la firma digitale può essere generata direttamente dall’interno del sistema di sviluppo, Vediamo come.
Abbiamo generato una soluzione Vuota, chiamata DnwStrongName all’interno di Visual Studio.net 2005 (professional edition), aggiungiamo un progetto di tipo C# Class Library che chiamiamo Dnw.StrongNamedCs, apriamo le property del progetto, facendo doppio click sulla cartella Properties nel Solution Explorer e selezioniamo il tab Signing. Come potete osservare nell’immagine, selezionando la checkbox Sign the Assembly, si attiva la combobox che indica di “selezionare un file chiave per lo strong name”, come mostrato nell’immagine, è possibile crearne uno nuovo oppure selezionarne uno esistente.
Se selezioniamo l’opzione New, Visual Studio ci permette di generare un nuovo KeyFile, contenente una coppia di chiavi per la firma digitale degli Assembly.
come possiamo osservare, l’utility di Visual Studio genera un file di tipo SNK, che contiene la coppia di chiavi crittografiche necessaria a firmare digitalmente l’Assembly della nostra DLL.
La stessa chiave crittografica, può essere utilizzata per firmare più Assembly, di seguito due immagini che ci mostrano come aggiunto un progetto Class Library in Visual Basic, Dnw.StrongNamedVb, utilizziamo l’opzione <Browse> per selezionare il file SNK appena creato per firmare anche il secondo Assembly:
Questa seconda modalità di acquisizione della coppia di chiavi crittografiche, è quella più utilizzata, è infatti consigliabile ed auspicabile, per chi sviluppa progetti .NET, firmare digitalmente tutti gli Assembly che produce con la stessa coppia di chiavi. Una volta generata la prima volta, salvarla in una cartella a parte (e farne una copia su cd) ed utilizzare sempre la stessa chiave per la firma di tutti gli Assembly.
Facciamo notare che, se osserviamo il Solution Explorer del secondo progetto, Visual Studio crea una copia del file delle chiavi crittografiche per ciascun Assembly che viene firmato.
Quando nelle proprietà di progetto è stata impostata la coppia di chiavi per la generazione dello strong name, il compilatore automaticamente estrae dal file delle chiavi la chiave pubblica e la colloca all’interno del file compilato, rendendola un estensione del nome dell’Assembly (a livello di chiamate del Framework). Questa operazione, rende l’Assembly prodotto da un programmatore, o da un team di programmatori, unico, in quanto solo chi ha accesso alla coppia di chiavi crittografiche potrebbe produrre un Assembly con lo stesso Strong Name.
Domanda: e se non volessi che la mia chiave crittografica fosse a disposizione di tutti i programmatori, ma solo di un supervisore che specificamente si occupa della firma digitale? Risposta: un attimo, di pazienza, ci arriviamo.
Cosa succede se perdiamo la nostra coppia di chiavi crittografiche?
Considerato che ne abbiamo fatto almeno due copie su CD, e che come abbiamo visto, Visual Studio ne crea una copia per ogni Assembly firmato, dobbiamo essere particolarmente sbadati o particolarmente sfortunati per perderla.
Se però accadesse cosa succede? Dobbiamo creare una nuova coppia di chiavi crittografiche, ed utilizzarla per firmare tutti i nostri Assembly, quando aggiorneremo i nostri programmi sui computer dei nostri clienti, le nuove librerie saranno regolarmente caricate. Se avessimo una applicazione che usa le vecchie librerie, nessun problema, continuerà a funzionare se installata solo in modo locale. Qualche problema potrebbe esservi se invece carichiamo le nostre librerie nella GAC (Global Assembly Cache) perché avendo due diversi Strong Name, sia le Vecchie librerie che le nuove librerie rimarranno al loro posto, potrebbero esservi anomalie a livello di applicazioni se queste ultime usassero versioni diverse delle librerie stesse, in questi casi, sarà necessario indicare specificamente all’applicazione le DLL da caricare intervenendo a livello di Application.config.
Come il compilatore costruisce lo Strong Name
Quando il compilatore firma digitalmente un Assembly, calcola un riassunto crittografico del contenuto dell’Assembly.
Un riassunto crittografico è uno hash del contenuto del file dell’Assembly,
che possiamo chiamare: riassunto di compilazione dell’Assembly. Il compilatore cifra il riassunto di compilazione usando la chiave privata presa dal file che contiene la coppia di chiavi crittografiche e memorizza questo riassunto cifrato all’interno dell’Assembly.
In seguito, ogni volta che il .NET loader carica un’Assembly firmato, calcola un riassunto crittografico dei contenuti dell’Assembly, che possiamo chiamare: il riassunto del runtime dell’Assembly.
Il loader, quindi, estrae dall’Assembly il riassunto di compilazione; ricava la chiave pubblica dall’Assembly stesso, e usa la chiave pubblica per decifrare il riassunto del compilatore precedentemente cifrato.
Fatto questo, il loader,confronta i due riassunti. Se non sono uguali, qualcosa o qualcuno ha modificato l’Assembly da quando è stato compilato; perciò, il runtime ferma il caricamento dell’Assembly.
Osservando quanto descritto qui sopra possiamo affermare che, se il programmatore non ha accesso alla chiave privata, non può permettere al compilatore di firmare l’Assembly.
In questo caso è forzatamente necessario l’uso della Firma Posticipata (Delay-sign) dell’Assembly.
Usare questa opzione, per cui è comunque necessario possedere la chiave pubblica dell’assembly, indica al compilatore che l’Assembly verrà dotato del riassunto crittografato in seguito. Ma significa anche che, poiché l’Assembly Delay-signed non contiene una firma digitale valida, il Loader del runtime del Framework non lo caricherà, non sarà quindi possibile eseguirlo o debuggarlo.
Domanda: E allora come si fa? Risposta: Un attimo di pazienza, ci arriviamo.
Firmare gli Assembly, a cosa serve? Ma perché farlo?
Il motivo primario per cui è opportuno firmare gli Assembly dei propri progetti Software è quello indicato al punto precedente, ovvero fare in modo che i propri Assembly non possano essere manipolati dopo essere stati compilati.
Un’altro motivo per firmare gli Assembly è quello di installare una versione unica delle librerie comuni a più applicazioni, ponendole nella Global Assembly Cache (GAC).
Installare le librerie comuni a più applicazioni nella GAC è anche una funzionalità molto comoda per gli sviluppatori per fare in modo che queste librerie siano sempre a disposizione ed aggiornate automaticamente per tutte le applicazioni dopo la compilazione del progetto che le contiene.
L’ultima, non meno importante, motivazione per firmare un Assembly, è quella di mettere le librerie .NET in grado fornire COM interoperability, ovvero permettere ad una applicazione COM di accedere alle funzionalità di una DLL .NET.
So che qui, i puristi di .NET potrebbero dire, “Ma noi lavoriamo solo con .NET!”, assolutamente encomiabile, ma se per caso, in un progetto di integrazione con altre applicazioni, doveste permettere ad una applicazione di utilizzare delle funzionalità sviluppate in .NET cosa fate?
Un piccolo esempio: Una delle mie applicazioni, gestisce l’acquisizione di dati di produzione con una sua interfaccia utente che viene agganciata da un esistente progetto Access.2000 via shell. Nella sperimentazione, ci siamo accorti che il programma Access, ha la necessità di generare dati anche senza intervento utente. Far generare dei record ad Access utilizzando delle Stored Procedure, avrebbe significato scrivere (duplicando) il codice già presente in .NET all’interno di Access e inoltre condizionare eventuali aggiornamenti dell’applicazione .NET ad un aggiornamento dell’applicazione Access. Oppure, generare una nuova applicazione chiamabile da Access per effettuare l’operazione di inserimento dati, ma considerato che Access doveva eseguire l’aggiornamento dati in una serie di passaggi transazionali, andare via Shell oppure costruire un servizio avrebbe reso ad Access impossibile capire se la porzione di transazione .NET fosse andata a buon fine se non utilizzando criteri empirici. La soluzione è stata quella di rendere accessibile tramite una DLL .NET una singola funzione che Access chiama indicando a .NET quali sono i dati da elaborare che una volta eseguita, rende ad access un OK oppure un Errore, in questo modo abbiamo ottenuto ciò che serviva con la sola necessità di inserire in GAC le dll .NET necessarie. E’ ovvio che creare e manutenere una DLL in grado di funzionare correttamente in GAC significa dover avere un po’ più di attenzione a ciò che gli sviluppatori fanno, (non cancellare classi, non rinominare metodi, usare l’obsolescenza invece che eliminare oggetti non più usati in versioni successive della DLL), ma i vantaggi sono sicuramente maggiori degli svantaggi.
Cosa accade se una azienda non vuole dare accesso alle chiavi private a tutti i programmatori?
Rispondiamo alle due domande che abbiamo rinviato nei paragrafi precedenti.
In questo caso, la gestione del lavoro dei programmatori è più complicata. Per prima cosa, è necessario estrarre la chiave pubblica dalla coppia di chiavi e rendere questa disponibile ai programmatori ( abbiamo visto come fare all’inizio dell’articolo). Gli Assembly devono quindi essere firmati con l’opzione Firma Posticipata (Delay-Sign) che trovate nella finestra Signing delle proprietà (MyProject) del progetto.
Faccio notare come sia specificamente indicato che un Assembly con l’opzione Delay Sign selezionata non può essere eseguito ne sarà debuggabile.
Qui sopra l’errore che ci da il debugger cercando di lanciare l’applicazione del nostro progetto di test dopo aver selezionato la Firma Posticipata.
Ovviamente, questo non significa che tutti i programmatori, per debuggare le loro applicazioni devono chiamare il responsabile in possesso della chiave privata, fargli firmare le loro DLL e i loro EXE e poi debuggarli, il problema può essere aggirato, sulle macchine di sviluppo utilizzando sempre l’utility sn.exe con il seguente comando.
sn.exe –Vr NomeAssembly.dll sn.exe –Vr NomeAssembly.exe
Per tornare alla situazione normale, ovviamente è indispensabile utilizzare il comando opposto:
sn.exe -Vu NomeAssembly.dll sn.exe -Vu NomeAssembly.exe
Tecnicamente, questa operazione aggancia all’Assembly un informazione che dice al Runtime del .NET Framework di non verificare lo Strong Name dell’Assembly, questa informazione rimane agganciata all’Assembly fino a che non viene rimossa con l’opzione -Vu è quindi importante prima che l’Assembly sia incluso in un pacchetto di installazione rimuovere questa funzionalità. Un esempio per poterlo fare è gestire la sua assegnazione, rimozione nel PostBuildEvent del progetto usando un semplice comando batch come questo:
if $(ConfigurationName) == Release goto rel "C:\Programmi\Microsoft Visual Studio 8\SDK\v2.0\Bin\sn.exe" –Vr Dnw.StrongNamedCs.dll goto Fine :Rel "C:\Programmi\Microsoft Visual Studio 8\SDK\v2.0\Bin\sn.exe" –Vu Dnw.StrongNamedCs.dll goto Fine :Fine
In questo comando, controlliamo qual’è la configurazione del progetto, se il progetto è in Debug, togliamo il controllo sullo strong name, se il progetto è in Release lo ripristiniamo. Questo, produce l’output desiderato per il programmatore durante il debug, e l’output corretto per chi si occupa poi della creazione del Package finale.
Una volta che l’applicazione è stata debuggata ed è pronta, chi è in possesso della Chiave Privata come fa a firmare gli Assembly?
Sicuramente sostituire tutti i files snk e ricompilare sarebbe inefficiente, inoltre, potrebbe produrre copie indesiderate della chiave che si vuole tenere privata.
In questo caso, la soluzione è semplice, basta utilizzare ancora una volta l’utility sn.exe:
sn.exe -R NomeDellaDll.dll DotNetWorkKeys.snk
Con questo comando, l’utility calcola il riassunto crittografico dell’Assembly, lo crittografa e lo inserisce nell’Assembly aggiungendo la chiave pubblica, rendendo quindi l’Assembly correttamente funzionante su qualsiasi sistema.
Quindi, nel caso si applichi questo tipo di politica aziendale, basta che la macchina su cui vengono effettuate le compilazioni definitive sia in grado di creare gli Assembly in modalità release e venga predisposto un Batch File che esegua la firma di tutti gli Assembly prima della creazione dei Package per il setup.
Qual’è il motivo per cui offuscare un Assembly crea dei problemi se questo è firmato?
Conoscendo che cosa fa un Obfuscator, è abbastanza facile comprenderlo, infatti, per offuscare un Assembly, il programma disassembla l’Assembly al suo Intermediate Language e va a fare delle modifiche per rendere incomprensibili tutti gli identificatori e tutto ciò che può rendere semplice a chi disassembla comprendere come funziona il programma. Un offuscatore, ha bisogno che, l’Assembly che deve elaborare, sia un Assembly valido per il Framework, quindi deve per forza essere firmato oppure deve essere Delay Signed e deve essere disattivato il suo controllo.
Dopo l’intervento dell’offuscatore, visto ciò che fa, è ovvio comprendere come la firma digitale dell’Assembly non sarà più valida, perché il suo hash sarà diverso da quello calcolato dal compilatore.
E’ quindi indispensabile, che dopo l’operazione di offuscamento sia utilizzata nuovamente l’utility per firmare digitalmente l’Assembly e renderlo valido per il Loader del Framework .NET.
Pertanto, per gli Assembly che si vogliono offuscare il procedimento potrebbe essere il seguente:
- Compilare l’Assembly con delay signing
- Disattivare il controllo dello strong name dell’Assembly
- Debuggare e verificare che funzioni
- Offuscare l’Assembly
- Debuggare e verificare che funzioni
- Attivare il controllo dell’Assembly
- Firmare digitalmente l’Assembly
Oppure:
- Compilare l’Assembly con strong name
- Debuggare e verificare che funzioni
- Offuscare l’Assembly
- Disattivare il controllo dello strong name dell’Assembly
- Debuggare e verificare che funzioni
- Attivare il controllo dell’Assembly
- Firmare digitalmente l’Assembly
Conclusioni
E’ assolutamente indispensabile utilizzare lo Strong Name per le proprie applicazioni in modo tale che non possano essere modificate dall’esterno (Virus ecc.).
Offuscare gli Assembly è utile, ma non è necessario farlo se non per le parti dei programmi che possiamo ritenere davvero particolarmente importanti per l’azienda, infatti, per quanto un programmatore possa essere bravo, scrivere ottimo codice e lavorare assolutamente bene, ritengo che sia inutile offuscare la User Interface che costruisse, così come inutile è offuscare tutte le parti di business logic ovvie, quali ad esempio i data provider (Leggere e scrivere su una tabella di database è operazione che chiunque sa fare).
Invece gli algoritmi di business logic, che sono il cuore di una applicazione, possono, anche se non tutti, meritare l’offuscamento.
Ricordo che Assembly offuscato non è sinonimo di Assembly non decompilabile e quindi incomprensibile, offuscato è sinonimo di Assembly che rende la vita difficile a chi vuol copiare un algoritmo.
Per una protezione maggiore di un algoritmo dalla Reverse Engineering, non c’è altro che la compilazione in linguaggio macchina, che comunque, un qualsiasi bravo programmatore, con una decente conoscenza dell’Assembler e una copia del vecchio CodeView Microsoft può decifrare.
Per qualsiasi Feedback, Ulteriore domanda, Chiarimento, oppure se trovate qualche errore usate direttamente il form dei commenti in calce a questo articolo.
Il codice sorgente del progetto di esempio usato per questo articolo può essere scaricato usando il link sottostante:
Companion code, Assembly Strong Named
Bibliografia: Giving a .NET Assembly a Strong Name; How Do I…Create an Assembly with a strong name?; How Do I…Create a delay signed shared Assembly? ; MSDN Assembly.Fullname; MSDN Assembly.GetExecutingAssembly; MSDN Assembly.GetName