versione italiana versione italiana
english version english version

Assembly: Versioning e Deployment (1/5)

L’assembly è l’unità di distribuzione in .NET Framework ed esprime un concetto logico, non fisico: infatti può essere composto da uno o più file, ma per .NET un assembly è una singola unità di deployment, contrassegnata da un solo numero di versione.

Maggiori informazioni sulla composizione di un assembly sono disponibili in Cosa è un assembly.

In questi articoli analizziamo le caratteristiche di versioning e deployment degli assembly in .NET. Vedremo che un assembly può avere uno strong name ed essere firmato digitalmente, garantendo così l’integrità dell’assembly nella distribuzione come componente privato o condiviso. Valuteremo le diverse modalità di distribuzione e gli scenari possibili dal punto di vista del versioning, analizzando l’impatto del rilascio di versioni successive dello stesso assembly rispetto a programmi esistenti che ne fanno uso.

 

Gli articoli collegati sono:

Strong Name

Uno strong name (nome sicuro nella traduzione italiana di Visual Studio) è prima di tutto un meccanismo di individuazione univoca degli assembly. Un sinonimo talvolta usato è shared name, proprio per indicare che tale nome individua con certezza un assembly.

Lo strong name è composto da quattro parti:

  • Nome
  • Versione
  • Cultura
  • Public Key Token (hash della chiave pubblica, con cui si verifica la firma digitale)

 

Lo strong name assolve due compiti:

  1. Garantire che il riferimento ad un assembly condiviso sia certo.
  2. Garantire che l’assembly non sia stato modificato dopo il suo rilascio.

 

Vedremo tra poco quali sono i meccanismi con cui si realizzano questi due obiettivi. Prima però facciamo una piccola digressione sulle differenze rispetto a COM, dove l’univocità di un componente veniva definita da un GUID che identificava univocamente una classe. Questa soluzione, benché funzionante, nel tempo ha evidenziato molti problemi:

  • Versioni diverse dello stesso componente COM devono mantenere la compatibilità binaria con le versioni precedenti, perché era impossibile (almeno fino all’introduzione dei componenti side-by-side) installare diverse versioni dello stesso componente sulla stessa macchina.
  • Le applicazioni che usano componenti COM non manifestano automaticamente una buona diagnostica degli errori quando il componente richiesto non è installato o è presente una versione incompatibile. In altre parole, il CLSID di un componente non fornisce molte informazioni sul componente stesso (come ad esempio il nome del componente).
  • La gestione di versioni localizzate di un componente era demandata al produttore del componente; il sistema operativo non offriva un meccanismo predefinito per far convivere sulla stessa macchina diverse versioni localizzate dello stesso componente.

 

Come abbiamo visto, lo strong name definisce prima di tutto un riferimento univoco ad un assembly (come faceva il vecchio CLSID). Questo significa che gli utilizzatori di un assembly con strong name dovranno specificare 4 coordinate (nome, versione, cultura e public key token) per richiamare l’assembly desiderato (anche quando è usato in modo privato).

Il secondo effetto che si ottiene fornendo uno strong name ad un assembly è di effettuare una verifica della firma digitale dell’assembly: qualsiasi modifica dei moduli che compongono l’assembly farà fallire tale controllo (in realtà il controllo dei moduli secondari è diverso da quello del modulo che contiene il manifest, come vedremo in Strong name e assembly multi-modulo). Tale controllo (validazione dello strong name) avviene quando l’assembly viene installato nella GAC (Global Assembly Cache), oppure ogni volta che l’assembly viene caricato in memoria se è privato.

Un’altra conseguenza dello strong name è che un assembly con strong name può referenziare solo assembly dotati a loro volta di strong name, senza poter più referenziare assembly che ne sono privi. Questa limitazione assicura che, utilizzando un assembly dotato di strong name, non siano richiamati assembly privi di firma digitale. Un assembly con strong name deve essere validato, quindi anche quelli che referenzia devono ottemperare agli stessi requisiti.

 

Vediamo come creare un assembly con uno strong name.

DIR: ASSEMBLY_SN       FILE: FOOASM.CS
using System;
using System.Reflection;
[assembly:AssemblyKeyFile("publicprivate.snk")]
[assembly:AssemblyDelaySign( false )]
public class FooAsm {
public static string GetName() {
return "Answer:FooAsm";
}
}

L’attributo AssemblyKeyFile fa riferimento al file publicprivate.snk, che contiene una coppia di chiavi pubblica/privata. Tale file va generato dal produttore della libreria con il comando:

> SN –k publicprivate.snk

Il secondo attributo, AssemblyDelaySign, serve a definire se effettuare la firma digitale contestualmente alla compilazione (false) oppure se rimandarla ad una fase successiva (true), come vedremo più avanti.

Questi due attributi vengono generati automaticamente quando si crea un progetto con Visual Studio.NET, in un file chiamato AssemblyInfo.

In questo momento non abbiamo specificato due parti dello strong name: la versione e la cultura. Vedremo questi attributi più avanti; la loro assenza determina la produzione di un assembly con versione 0.0.0.0 e cultura neutrale.

Compiliamo l’assembly:

> csc /target:library fooasm.cs

 

Per provare l’assembly abbiamo bisogno di un programma di prova.

DIR: ASSEMBLY_SN       FILE: FOOTEST.CS
using System;
public class FooTest {
public static void Main() {
Console.WriteLine( "Asm = {0}", FooAsm.GetName() );
}
}

Compiliamo questo programma facendo riferimento all’assembly precedente:

> csc footest.cs /r:fooasm.dll

Possiamo ora eseguire footest.exe e ammirare il corretto funzionamento del tutto.

Fino a questo momento, non c’è nessuna differenza visibile nel comportamento di quanto abbiamo ottenuto rispetto a quello che sarebbe successo compilando l’assembly senza i due attributi AssemblyKeyFile e AssemblyDelaySign. In realtà il risultato è molto diverso. Da una parte abbiamo l’assembly fooasm.dll che possiede uno strong name; dall’altra abbiamo l’applicazione footest.exe (anch’essa è un assembly) che contiene un riferimento ad uno strong name.

Partiamo dall’assembly fooasm.dll; il fatto di avere uno strong name significa che l’assembly è stato firmato digitalmente.

La figura che segue rappresenta questo processo.

Durante la compilazione viene letto il file contenente la chiave pubblica e privata; la chiave pubblica viene copiata nei metadati dell’assembly, mentre la chiave privata viene usata, insieme ad un hash del file, per calcolare la firma digitale (signature) salvata nell’assembly.

La figura che segue illustra meglio questo meccanismo: il contenuto di tutto il file che contiene il manifest, esclusa la zona che conterrà la signature, viene letto da un algoritmo che genera un valore di hash del file (hash file). Questo valore viene firmato con la chiave privata e la signature risultante viene salvata nello spazio riservato nell’assembly.

Nella fase di validazione dello strong name il runtime di .NET calcola il valore hash dell’assembly (esclusa la signature) e verifica la corrispondenza della signature rispetto alla chiave pubblica. Dunque, se il file viene alterato successivamente alla firma digitale (che prima abbiamo effettuato durante la compilazione), la modifica invaliderà la signature, facendo fallire la validazione dello strong name. L’unica possibilità è ri-firmare l’assembly con la chiave privata, ottenendo così una nuova signature valida. Dunque è fondamentale che la chiave privata sia custodita con la massima cura da parte dell’autore del componente.

Riferimenti ad assembly con strong name

Occupiamoci ora dell’applicazione che referenzia un assembly con strong name. Il riferimento ad un assembly esterno avviene sempre specificando nei metadati il nome dell’assembly e la versione. Nel caso di un riferimento verso uno strong name, oltre a queste informazioni viene memorizzato un contrassegno ridotto della chiave pubblica (public key token), che occupa solo 8 byte; a scopo di verifica possiamo visualizzare il public key token della chiave pubblica creata in precedenza, prima estraendo la sola chiave pubblica nel file public.snk:

> SN –p publicprivate.snk public.snk

per poi visualizzare il valore del token della chiave pubblica con:

> SN –t public.snk

La stringa esadecimale visualizzata corrisponde al valore di public key token che si trova nei metadati di footest.exe. Questa è una parte dei metadati estratta con ILDASM; il valore effettivo che troverete sarà ovviamente diverso perché dipende dalle chiavi che generate.

MANIFEST – footest.exe
.assembly extern FooAsm
{
.publickeytoken = (11 E5 57 1F 22 DF E9 99 )                          //    ..W."...
.ver 0:0:0:0
}

Solo a questo punto le informazioni sulla versione dell’assembly referenziato cominciano ad essere significative. Quando si fa riferimento ad un assembly senza specificare il public key token, di fatto l’assembly può solo essere privato. Poiché non c’è nessun controllo di versione per gli assembly privati, ne consegue che l’indicazione della versione per un assembly privo di strong name è a titolo puramente indicativo perché, di fatto, non determina nessun vincolo e/o controllo in fase di caricamento dell’assembly utilizzato.

Come abbiamo visto, il public key token viene estratto automaticamente durante la compilazione degli assembly che utilizzano un assembly dotato di strong name. Questo significa che, in fase di compilazione, bisogna referenziare esattamente la versione di assembly che si desidera utilizzare in fase di esecuzione. Approfondiremo questo aspetto più avanti, parlando dei problemi che possono sorgere nell’ambito di team di sviluppo piuttosto grandi.

Validazione dello strong name

La validazione dello strong name è un’operazione che convalida la validità della signature di un assembly rispetto al valore di hash del file che contiene il manifest dell’assembly e alla chiave pubblica contenuta nello stesso file. Da notare che la chiave pubblica fa parte del contenuto del file sottoposto a funzione di hash, mentre ne sono esclusi i byte che compongono la signature.

Tale operazione avviene in momenti diversi, a seconda che l’assembly sia condiviso o privato:

  • Per un assembly privato, la validazione avviene quando l’assembly viene caricato in un application domain.
  • Per un assembly condiviso, la validazione avviene quando l’assembly viene installato nella GAC.

La validazione è un’operazione costosa, perché richiede la lettura di tutto il contenuto di un modulo dell’assembly (e spesso gli assembly sono costituiti da un solo file) per riuscire ad elaborare il valore di hash. Per questo motivo tale operazione non è ripetuta ad ogni caricamento per gli assembly installati nella GAC: l’assunzione di fondo è che tutte le installazioni di assembly condivisi possono essere effettuate solo da utenti autorizzati, quindi spostando il problema alla fase di installazione si migliorano le prestazioni in fase di esecuzione.

L’altra faccia della medaglia è che un utente che può accedere alla GAC (che non è altro che una particolare directory del sistema operativo, tipicamente \windows\assembly\GAC o \winnt\assembly\GAC) può potenzialmente modificare tutti gli assembly condivisi, senza che la validazione degli strong name abbia un qualche effetto in futuro. Ciò ad oggi non costituisce una particolare vulnerabilità del sistema, perché non è un rischio maggiore di quello che si corre rispetto ai file di sistema di Windows. Allo stesso tempo, però, bisogna essere consci del funzionamento del sistema che si utilizza, per non fare assunzioni errate in situazioni particolari.

Delay signing (firma digitale posticipata)

Facciamo alcune modifiche al codice dell’assembly fooasm in modo da posticipare la firma digitale.

DIR: ASSEMBLY_SN_DELAY FILE: FOOASM.CS
using System;
using System.Reflection;
[assembly:AssemblyKeyFile("public.snk")]
[assembly:AssemblyDelaySign( true )]
public class FooAsm {
public static string GetName() {
return "Answer:FooAsm";
}
}

Entrambi gli attributi sono cambiati, AssemblyDelaySign indica che la firma digitale è posticipata, mentre AssemblyKeyFile fa ora riferimento al file public.snk, che deve contenere la sola chiave pubblica della coppia di chiavi usata in precedenza.

Il comando che segue consente di estrarre la chiave pubblica nel file public.snk:

> SN –p publicprivate.snk public.snk

Il proprietario della chiave privata potrà in seguito firmare l’assembly con il comando:

> SN –R fooasm.dll publicprivate.snk

Lo schema che segue riassume un po’ meglio quello che succede:

Dopo la compilazione, l’assembly fooasm.dll contiene una chiave pubblica e può essere referenziato da altri assembly, ma, se ne viene tentato il caricamento, la validazione dello strong name fallisce. In seguito all’operazione di firma dell’assembly con la chiave privata si ottiene un assembly corrispondente a quello ottenuto firmando l’assembly contestualmente alla compilazione.

Il vantaggio di quest’operazione divisa in due passi è che la chiave privata può essere custodita in un luogo sicuro, senza essere messa a disposizione di tutti gli sviluppatori. Ovviamente bisogna fare in modo che gli sviluppatori che non dispongono della chiave privata possano provare gli assembly compilati con la sola chiave pubblica e privi di signature valida. Questo è possibile disabilitando la validazione di un particolare assembly, con il comando:

> SN –Vr fooasm.dll

Tale commando va usato solo durante lo sviluppo, ed è annullabile con:

> SN –Vu fooasm.dll

Per disabilitare la validazione di tutti gli assembly con una certa chiave pubblica, si può usare questa sintassi:

> SN –Vr *,<public key token>

dove a <public key token> si sostituisce la stringa esadecimale ottenuta con il comando:

> SN –t public.snk

La possibilità di disabilitare tutti gli assembly con la stessa chiave pubblica è molto comoda, perché tutti i componenti sviluppati da un’azienda possono essere firmati con la stessa coppia di chiavi, diventando così facilmente riconoscibili. Bisogna tuttavia considerare che lo strong name non garantisce l’identità dell’autore, funzionalità ottenibile con il meccanismo di Authenticode.

Strong name e assembly multi-modulo

Analizziamo ora il comportamento di un assembly multi-modulo, composto da due moduli, fooasm e foomod.

DIR: ASSEMBLY_SN_MOD   FILE: FOOASM.CS
using System;
using System.Reflection;
[assembly:AssemblyKeyFile("publicprivate.snk")]
[assembly:AssemblyDelaySign( false )]
public class FooAsm {
public static string GetName() {
return "Answer:FooAsm";
}
}
DIR: ASSEMBLY_SN_MOD   FILE: FOOMOD.CS
using System;
using System.Reflection;
public class FooMod {
public static string GetName() {
return "Answer:FooMod v1";
}
}

Compiliamo l’assembly:

> csc /target:module foomod.cs

> csc /target:library /addmodule:foomod.netmodule fooasm.cs

A questo punto si ottengono due file, foomod.netmodule e fooasm.dll.

Per provare l’assembly creiamo un programma di prova.

DIR: ASSEMBLY_SN_MOD   FILE: FOOTEST.CS
using System;
public class FooTest {
public static void Main() {
Console.WriteLine( "Asm = {0}", FooAsm.GetName() );
Console.WriteLine( "Mod = {0}", FooMod.GetName() );
}
}

Compiliamo questo programma facendo riferimento all’assembly precedente (notare che non c’è nessun riferimento al modulo foomod):

> csc /r:fooasm.dll footest.cs

Eseguendo footest.exe otteniamo il risultato previsto.

Asm = Answer:FooAsm
Mod = Answer:FooMod v1

Ricordiamo che l’assembly è stato firmato digitalmente; proviamo ad alterare il modulo esterno per vedere cosa succede.

DIR: ASSEMBLY_SN_MOD   FILE: FOOMOD_V2.CS
using System;
using System.Reflection;
public class FooMod {
public static string GetName() {
return "Answer:FooMod v2";
}
}

Compiliamo soltanto il modulo sovrascrivendo quello precedente:

> csc /target:module /out:foomod.netmodule foomod_v2.cs

Senza ricompilare l’assembly né l’eseguibile che lo referenzia, proviamo ad eseguire nuovamente footest.exe e vediamo il risultato:

Unhandled Exception: System.IO.FileLoadException: The check of the module's hash
failed for file 'foomod.netmodule'.
File name: "foomod.netmodule"
at FooTest.Main()

Come potevamo aspettarci, non funziona: cerchiamo di capire perché.

Il manifest dell’assembly in fooasm.dll contiene un riferimento al file foomod.netmodule:

MANIFEST – fooasm.dll
.file foomod.netmodule
.hash = (6F A6 62 C7 0F 13 3F E3 27 80 A3 04 C6 B8 1E EB   // o.b...?.'.......
29 95 F2 11 )                                     // )...

Il riferimento al file è associato ad un valore hash. Tale valore è quello calcolato al momento della compilazione di fooasm.dll, quando il file foomod.netmodule era ancora alla sua prima versione. Quando abbiamo ricompilato questo modulo, sicuramente il valore hash del file è cambiato, perché abbiamo modificato la stringa restituita dalla funzione FooMod.GetName() nel sorgente foomod_v2.cs. Per aggiornare il valore hash del modulo anche nel manifest dell’assembly è necessario ricompilare l’assembly:

> csc /target:library /addmodule:foomod.netmodule fooasm.cs

Se torniamo ad analizzare il manifest in fooasm.dll, troveremo il nuovo valore hash del file foomod.netmodule:

MANIFEST – fooasm.dll
.file foomod.netmodule
.hash = (B0 45 95 99 79 FD CA 57 12 7C 2F 97 F8 70 3C 63   // .E..y..W.|/..p<c
77 90 D2 E1 )                                     // w...

Se si modificasse direttamente il valore hash nel file fooasm.dll originale (con un editor di file binario), si andrebbe comunque a invalidare la verifica della firma digitale dell’assembly che, come ci ricorderemo, è dotato di strong name.

Per questo motivo, se riproviamo a fare le stesse operazioni con un assembly privo di strong name, vedremo che il controllo del valore hash del file associato all’assembly non avviene.

Modifichiamo solo il sorgente di fooasm.cs, che per comodità chiamiamo ora fooasm_nosn.cs.

DIR: ASSEMBLY_SN_MOD   FILE: FOOASM_NOSN.CS
using System;
using System.Reflection;
public class FooAsm {
public static string GetName() {
return "Answer:FooAsm";
}
}

Compiliamo il tutto, compreso il programma di esempio, che non farà più riferimento ad un assembly con strong name (abbiamo rimosso gli attributi che generavano lo strong name).

> csc /target:module foomod.cs

> csc /target:library /addmodule:foomod.netmodule /out:fooasm.dll fooasm_nosn.cs

> csc /r:fooasm.dll footest.cs

Analizzando nuovamente il manifest di fooasm.dll, troveremo ancora un riferimento al file foomod.netmodule complete di hash:

MANIFEST – fooasm.dll
.file foomod.netmodule
.hash = (0B EF FE 0C 47 F4 07 DE F9 8E 76 C9 14 F8 BE B9   // ....G.....v.....
4E E7 41 98 )                                     // N.A.

Se eseguiamo footest.exe otterremo lo stesso risultato iniziale del test precedente:

Asm = Answer:FooAsm
Mod = Answer:FooMod v1

Proviamo a sostituire nuovamente foomod.netmodule con il risultato della compilazione di foomod_v2.cs, senza ricompilare né l’intero assembly, né il programma di prova:

> csc /target:module /out:foomod.netmodule foomod_v2.cs

Ora proviamo ad eseguire footest.exe:

Asm = Answer:FooAsm
Mod = Answer:FooMod v2

La seconda linea contiene la stringa restituita dal nuovo modulo. Il manifest di fooasm.dll nel frattempo non è cambiato, e fa riferimento ad un file con un valore di hash differente, ma questa prova ci ha dimostrato che la verifica del valore hash dei moduli collegati ad un assembly avviene solo se lo stesso assembly è dotato di strong name.

 

Aggiungiamo un’altra considerazione: se si posticipa la firma digitale dell’assembly, il controllo dei valori hash dei file che fanno parte degli assembly avviene ugualmente.

Proviamo a compilare un’ulteriore versione di fooasm.dll, questa volta senza firmare l’assembly con la chiave privata ma definendo comunque lo strong name:

DIR: ASSEMBLY_SN_MOD   FILE: FOOASM_DELAY.CS
using System;
using System.Reflection;
[assembly:AssemblyKeyFile("public.snk")]
[assembly:AssemblyDelaySign( true )]
public class FooAsm {
public static string GetName() {
return "Answer:FooAsm";
}
}

 

Per compilare l’assembly è necessario estrarre la chiave pubblica nel file public.snk; si ricompila il tutto e si disabilita la validazione dell’assembly fooasm.dll ottenuto.

> SN -p publicprivate.snk public.snk

> csc /target:module foomod.cs

> csc /target:library /addmodule:foomod.netmodule /out:fooasm.dll fooasm_delay.cs

> csc /r:fooasm.dll footest.cs

> SN -Vr fooasm.dll

A questo punto, al solito, verifichiamo l’output di footest.exe:

Asm = Answer:FooAsm
Mod = Answer:FooMod v1

Tutto bene; proviamo a sostituire il solo file foomod.netmodule con una versione diversa, come nelle prove precedenti:

> csc /target:module /out:foomod.netmodule foomod_v2.cs

Eseguendo footest.exe ci si potrebbe anche aspettare che la chiamata al nuovo modulo avvenga regolarmente, così come avveniva con l’assembly privato – tutto sommato abbiamo disabilitato la validazione dell’assembly. Invece otteniamo nuovamente l’eccezione provocata dalla mancanza di corrispondenza tra il valore hash del file foomod.netmodule memorizzato nel manifest dell’assembly ed il valore calcolato effettivamente dal file foomod.netmodule presente su disco al momento dell’esecuzione:

Unhandled Exception: System.IO.FileLoadException: The check of the module's hash
failed for file 'foomod.netmodule'.
File name: "foomod.netmodule"
at FooTest.Main()

Non è quindi possibile sostituire un modulo ad un assembly senza ricompilare il manifest dell’assembly, se l’assembly è dotato di strong name.

 

Dal punto di vista degli assembly installati nella GAC, esiste qualche differenza.

Come abbiamo già visto, la validazione di una assembly nella GAC avviene al momento della sua installazione; tutti i successivi caricamenti dell’assembly non produrranno alcuna operazione di validazione. Analogamente, anche la verifica degli hash dei moduli che compongono un assembly avviene al momento dell’installazione dell’assembly nella GAC, e non viene più effettuata per i successivi caricamenti dell’assembly da parte delle applicazioni che ne fanno uso. Ancora una volta bisogna ribadire quanto sia importante verificare una corretta impostazione dei diritti di accesso alle directory che ospitano la GAC.

 

Riepilogando quanto abbiamo visto, il controllo dei moduli associati ad un assembly avviene solo se l’assembly è dotato di strong name. È quindi possibile sostituire parte di un assembly (un modulo) solo se l’assembly è privato ed è senza strong name. Se l’assembly è dotato di strong name, la verifica degli hash dei moduli che fanno parte dell’assembly avviene indipendentemente dalla disabilitazione o meno della validazione dell’assembly tramite il comando SN -Vr. Tale verifica avviene al momento del caricamento di un assembly privato, o al momento dell’installazione di un assembly nella GAC.


L'articolo continua con GAC - Global Assembly Cache