.NET 2.0: Beveiliging in .NET 2.0
Door Michiel van Otegem
4 oktober 2008
.NET wordt door Microsoft geprofileerd als het platform voor het bouwen van "connected systems", applicaties die op
allerlei manieren met elkaar verbonden zijn. Voor het succes daarvan is de beveiliging uitermate belangrijk. De
mogelijkheden die .NET daarvoor biedt zijn dan ook zeer uitgebreid.
Beveiliging in .NET bestaat uit een aantal onderdelen. Het meest voor de hand liggend is beveiliging op basis van
gebruikers en rollen, zoals dat in elk modern operating system geregeld is. Een applicatie of (web) service kan
toegankelijk gemaakt worden voor bepaalde gebruikers of rollen door gebruik te maken van de standaard bestandpermissies
van Windows. Dit is echter een betrekkelijk grove manier van beveiliging, iemand heeft geen toegang of helemaal niet.
In een .NET applicatie kun je echter in de code aangeven welke gebruikers of rollen toegang hebben tot een component,
object, of functie. Dit kan zowel op basis van Windows accounts aan de hand van de WindowsPrincipal die automatisch
beschikbaar is als een applicatie uitgevoerd wordt. Het kan echter nodig zijn om juist geen gebruik te maken van
Windows accounts, maar te werken met een eigen database met gebruikers en rollen. In dat geval moet je zelf de
authenticatie van de gebruiker verzorgen, bijvoorbeeld aan de hand van een gebruikersnaam en wachtwoord combinatie,
of via alternatieven als een smart card of biometrische apparatuur. Het autoriseren van de gebruikers vindt dan niet
plaats op basis van de WindowsPrincipcal, maar aan de hand van de GenericPrincipal. In Afbeelding 1 staat een functie
die gebruikt wordt om een gebruiker in te loggen na het invullen van een gebruikersnaam en wachtwoord. Deze worden
eerst gecontroleerd. Hier expliciet op een bepaalde waarde, maar normaal gezien zou dat aan de hand van een database
of andere gegevensbron werken. Vervolgens worden de rollen bepaald van de gebruiker (ook voor het voorbeeld weer
expliciet), waarna een GenericPrincipal wordt aangemaakt. Die bestaat uit de identiteit die geverifieerd is door de
gebruikersnaam en het wachtwoord te controleren, en de rollen doe toegekend zijn. Als dit gebeurd is, wordt de
principal toegekend aan de Thread die de applicatie uitvoert, zodat deze bij het aanroepen van iedere functie
beschikbaar is.
1: Public Function Login(ByVal username As String,\
ByVal password As String) As Boolean
2: 'Naam en wachtwoord controleren (bijvoorbeeld met database)
3: If username <> "Michiel" Or password <> "wachtwoord" Then
4: 'Naam of wachtwoord onjuist
5: Return False
6: End If
7:
8: 'Rollen bepalen (bijvoorbeeld uit database halen)
9: Dim roles As String() = {"Gebruiker","Management"}
10:
11: 'Identiteit maken en nieuwe security principal maken
12: Dim userIdentity As New GenericIdentity(username)
13: Dim userPrincipal As New GenericPrincipal(userIdentity, roles)
14:
15: 'Principal toekennen aan uitvoerende thread
16: AppDomain.CurrentDomain.SetPrincipalPolicy(
PrincipalPolicy.UnauthenticatedPrincipal)
17: Thread.CurrentPrincipal = userPrincipal
18:
19: 'Status OK retourneren
20: Return True
21: End Function
Afbeelding 1, Visual Basic code voor het aanmaken van een GenericPrincipal
Op basis van een principal, of dit nu een WindowsPrincipal, GenericPrincipal, of een ander type principal is, kan in
code geverifieerd worden of de gebruiker toegang verleend moet worden. Een voorbeeld hiervan zie je in de functie
KnoppenInstellen in Afbeelding 2, waarin knoppen zichtbaar worden gemaakt aan de hand van de daarvoor benodigde rol.
Hoewel dat werkt, en in dit geval een handige manier is om de gebruikersinterface aan te passen aan de rol van de
gebruiker, kan dit voor functies handiger, zoals gedaan is bij de SluitBoekJaar functie. Daar wordt aan de hand van
een attribuut opgegeven wie toegang heeft tot de functie. De Common Language Runtime (CLR) verzorgt vervolgens de
controle. Deze manier van instellen noemen we declaratief, omdat we aangeven wat we willen. Hoe de controle plaatsvindt
is niet van belang. In KnoppenInstellen wordt de controle echter expliciet door de code gedaan.
1: Friend Sub KnoppenInstellen()
2: MaakRapportButton.Visible =
Thread.CurrentPrincipal.IsInRole("Management")
3: SluitBoekJaarButton.Visible =
Thread.CurrentPrincipal.IsInRole("Administratie")
4: End Sub
5:
6:
7: <PrincipalPermissionAttribute(
SecurityAction.Demand, Role := "Administratie")>
8: Friend Sub SluitBoekJaar()
9: 'Boekjaar sluiten (niet getoond)
10: End Sub
Afbeelding 2, Visual Basic code voor het controleren van een rol
Het voordeel van de beveiliging op basis van principals in .NET biedt mogelijkheden om de beveiliging veel fijnmaziger
op te zetten, omdat je per component, object, of functie kunt bepalen wie er wel of geen toegang toe hebben. Zo kan
dezelfde applicatie, zonder aanpassingen, gebruikt worden door verschillende gebruikers, terwijl ze verschillende
rechten hebben. In een urenadministratie applicatie kun je bijvoorbeeld reguliere gebruikers alleen toegang geven tot
het invoer scherm waarin ze hun uren moeten invullen. Administratief personeel kan via dezelfde applicatie de eigen
uren invullen, maar krijgt ook toegang tot de administratieve mogelijkheden. Het management tenslotte heeft toegang
tot de rapportage, maar heeft niet de administratieve mogelijkheden die het administratieve personeel heeft. De
mogelijkheid om applicaties op deze manier op te kunnen zetten vereenvoudigt zowel de ontwikkeling van de applicatie
als de uitrol ervan, omdat er maar één applicatie nodig is voor de verschillende doelgroepen. Bovendien kan de toegang
tot de onderdelen van de applicatie centraal geregeld worden, in plaats van op ieder werkstation, en kan iemand
ongeacht zijn/haar rol ieder werkstation gebruiken.
An sich is het mechanisme van principal gebaseerde beveiliging niet nieuw in .NET. In versie 1.x is dit ook al aanwezig,
maar er zijn een aantal verbeteringen. Zo kun je nu zowel bij het Windows account van de gebruiker op basis van de
gebruikersnaam, of op basis van de interne binaire representatie van Windows, en kun je de ene naar de andere
"vertalen". Dit is bijvoorbeeld handig in gedistribueerde applicaties, waarbij je niet wilt dat er account gegevens
over de lijn gaan. Dan is de binaire representatie een stuk veiliger. Afbeelding 3 geeft hier een simpel voorbeeld
van.
1: 'Binaire representatie ophalen
2: Dim sid As SecurityIdentifier;
3: sid = WindowsIndentity.GetCurrent().User;
4:
5: 'Vertalen naar NT account
6: Dim account As NTAccount;
7: account = CType(sid.Translate(TypeOf(NTAccount)), NTAccount)
8:
9: 'NT account domein en naam weergeven
10: MessageBox.Show(account.Value)
Afbeelding 3, Visual Basic code voor het vertalen tussen de verschillende account representaties
Een andere verbetering is de ondersteuning voor beveiliging op "objecten", zoals het bestandssyteem en het Windows
register. Wanneer de gebruiker voldoende rechten heeft kunnen die rechten zonodig gemanipuleerd worden voor anderen.
Een laatste verbetering heeft betrekking op de situatie waarin operaties uitgevoerd worden onder de context van een
andere gebruiker (impersonation). In versie 1.x is het niet mogelijk om te weten dat er sprake is van impersonation,
tenzij je met unmanaged code aan de slag gaat door een Win32 API aan te roepen. Dat is nu verholpen doordat je aankunt
geven of je de context gebruiker of de eigenlijke gebruiker wilt hebben.
Code Access Security
Op basis van de gebruiker bepalen wat er uitgevoerd mag worden is erg belangrijk, maar aan dat model ontbreekt nog wel
een stuk. Op basis van die beveiliging alleen kunnen we niet voorkomen dat kwaadaardige code uitgevoerd wordt, en zijn
systemen onder andere vatbaar voor virussen en spyware. Voor de komst van .NET was hier weinig tegen te doen, omdat
alle applicaties bestaan uit binaire executables. Alleen op basis van herkomst en/of aan de hand van een certificaat
kun je bepalen of je de betreffende applicatie of component wilt vertrouwen. .NET lost dit op doordat er sprake is van
"managed code", code die gecontroleerd wordt door de CLR. Op basis van de meta data die bij een component of applicatie
hoort kan de CLR bepalen of het veilig is om deze uit te voeren. Om te beginnen bevat .NET hiervoor enkele
voorgedefinieerde "permission sets" waarbinnen een component wordt uitgevoerd. De Internet permission set zie je in
Afbeelding 4. De rechten van deze permission set zijn beperkt tot een afgeschermde opslag (Isolated Storage File),
enkele user interface mogelijkheden, en printen. Alle andere zaken, zoals bijvoorbeeld toegang tot de Event Log,
toegang tot de benodigde onderdelen om een database te benaderen, en toegang tot het bestandssysteem zijn niet
beschikbaar voor die permission set. Daarmee hebben alle applicaties van het internet, niet alleen die in de context
van Internet Explorer werken, maar beperkte speelruimte. Pas als je een gedownloade applicatie kopieert naar een
andere locatie op de harde schijf krijgt deze de volledige rechten.

Afbeelding 4, Voorbeeld van Internet permission set
Het principe van permission sets is niet echt nieuw. De Java Virtual Machine (JVM) maakt hier ook gebruik van evenals
de eerdere .NET versies. Ten opzichte van versie 1.x zijn er uiteraard een aantal zaken verfijnd. .NET biedt de
mogelijkheid om zelf permission sets te definiëren, en biedt ontwikkelaars de mogelijkheid om in de code aan te geven
welke rechten het nodig heeft. Vandaar dat ook gesproken wordt over Code Access Security, omdat bepaald wordt welke
rechten de code heeft, onafhankelijk van welke gebruiker de applicatie uitvoert. Wanneer een ontwikkelaar aangeeft
welke rechten een component nodig heeft, kan tijdens de installatie, of tijdens het uitvoeren van een applicatie
gecontroleerd worden of de benodigde rechten wel aanwezig zijn. Zo niet, dan kan de gebruiker hiervan op de hoogte
gebracht worden. De gebruiker kan dan eventueel kiezen om een andere permission set toe te kennen aan de applicatie,
zodat deze wel de benodigde rechten heeft. Het is dan ook een best practice voor ontwikkelaars om componenten te
ontwikkelen op basis van “least privile”, oftewel zorgen dat een component alleen maar om die rechten vraagt die het
ook daadwerkelijk nodig heeft. De beste manier om dit voor elkaar te krijgen is om te beginnen met een component dat
geen enkele rechten heeft, en te bepalen welke rechten toegevoegd moeten worden om de component te kunnen uitvoeren.
De .NET 2.0 Software Developer Kit bevat permview.exe, waarmee ontwikkelaars kunnen bepalen welke rechten een component
nodig heeft op het moment dat dit nog niet gespecificeerd is. Visual Studio 2005 bevat een uitgebreidere versie in de
vorm van permcalc.exe. Het nadeel van deze tools is dat ze een hele bak met informatie over je heen storten waar je
wel even wijs uit moet worden.
In de code kan op verschillende manieren aangegeven worden wat er nodig is om de code uit te kunnen voeren. Op
component (assembly) niveau kan aangegeven worden wat er voor de component als geheel geldt. Hierbij kan gerefereerd
worden aan een van de ingebouwde permissions sets, als volgt:
<assembly: PermissionSetAttribute(SecurityAction.RequestMinimum,
Name="Internet">
Ook kun je per sort permissie aangeven dat die nodig is, bijvoorbeeld zoals hieronder voor toegang tot een specifiek tekst bestand.
<assembly: FileIOPermissionAttribute(SecurityAction.RequestMinimum,
ViewAndModify="c:\voorbeeld\voorbeeld.txt">
In de bovenstaande voorbeelden wordt met SecurityAction.RequestMinimum aangegeven dat dit een minimum vereiste is om
de applicatie uit te kunnen voeren. Als alternatief kun je ook aangeven dat deze permissie optioneel is. Daarmee geef
je aan dat de applicatie beter werkt als het wel mag, maar ook werkt als dat niet zo is. Interessant is ook de optie
om juist aan te geven dat een component een bepaalde permissie niet nodig heeft. Het voordeel hiervan is dat in het
geval van een beveiligingsprobleem je heel snel kunt bepalen welke componenten in elk geval niet verantwoordelijk zijn
voor het probleem. De componenten die de permissies niet hebben om de gewraakte operatie uit te voeren kunnen meteen
uitgesloten worden. Bedrijven die componenten ontwikkelen voor andere partijen, die eventueel ook gebruikt worden in
combinatie met componenten die zij zelf niet gemaakt hebben kunnen zich hier enorm veel ellende (aansprakelijkheid)
besparen.
Cryptografie
Als gevoelige gegevens niet versleuteld zijn, dan kunnen andere daar onherroepelijk bij. Op welke manier je een
applicatie dan ook beveiligd, uiteindelijk heeft dat weinig nut. Je zult de gegevens in die gevallen dus moeten
versleutelen. .NET bevat een heel scala aan mogelijkheden als het gaat om cryptografie. Hieronder valt Public Key
Encryption (PKE), hashing, en andere vormen van cryptografie. Een van de nadelen in .NET 1.1 is dat je de key
(sleutel) direct gebruikt. Er is geen ondersteuning voor X509 certificaten. In .NET 2.0 is dit verholpen en kun je
gebruik maken van de certificaten in de X509 Certificate Store.
Een handige en eenvoudige toevoeging aan het .NET Framework is verder de ondersteuning voor zogenaamde secure strings.
Normaal gezien wordt string data in het geheugen gezet op een manier die leesbaar is voor andere applicaties. Iemand
die dus in het geheugen kan snuffelen zou wel eens op gevoelige data, waaronder wachtwoorden, kunnen stuiten. De
secure string zorgt ervoor dat dit niet kan door de strings versleuteld in het geheugen op te slaan. Dit brengt
uiteraard wel wat overhead met zich mee, maar beveiliging mag wat kosten, niet?
Op het vlak van Web Services is de beveiliging ook uitgebreid met mogelijkheden om gegevens te versleutelen. Deze
maakten al onderdeel uit van de Web Services Enhancements die een tussentijdse oplossing bieden voor Web Services
technologie die standaard nog geen deel uit maakt van het .NET Framework. Alles wat tot nog toe onder Web Services
Enhancements is uitgebracht maakt onderdeel uit van .NET 2.0.
Remoting, ASP.NET en ClickOnce
Een van de nadelen van de meeste beveiliging is dat deze in applicaties zelf verwerkt zit. Het is daardoor niet
mogelijk om de manier waarop een applicatie beveiligd is te veranderen wanneer een applicatie eenmaal uitgerold is.
In .NET 2.0 geldt dit voor een aantal onderdelen niet, en dan met name ASP.NET en .NET Remoting. Remoting wordt
gebruikt voor gedistribueerde applicaties waarbij bepaalde functionaliteit door een andere server (of servers)
verzorgd wordt. Een belangrijk aspect daarvan is uiteraard de beveiliging van de communicatie tussen de servers. In
.NET 1.1 kon dit eigenlijk alleen maar door de functionaliteit onder Internet Information Server (IIS) aan te bieden.
De beveiliging van IIS kan dan gebruikt worden, maar je bent wel gebonden aan communicatie via HTTP. Wil je dit niet,
dan moet je de beveiliging helemaal zelf verzorgen en ben je wel even zoet. Met .NET 2.0 is dit verleden tijd doordat
Remoting nu een eigen faciliteit biedt voor zowel authenticatie als de encryptie van het communicatiekanaal. Omdat
dit in het configuratiebestand geregeld kan worden kan dit ook als de applicatie geïnstalleerd is nog gewijzigd worden.
Wat dat betreft is dit al een beetje een voorschotje op het communicatiemechanisme in Windows Vista dat bekend staat
onder de codenaam Indigo. Indigo moet alle bestaande mechanismen in Windows vervangen en in Indigo is vrijwel ieder
onderdeel van de interactie tussen twee punten te configureren, van transport mechanisme tot ondersteuning van
transacties en beveiliging.
Wat betreft het kunnen configureren van de beveiliging slaat ASP.NET 2.0 echt wel alles. De beveiliging van een web
applicatie kan geheel geregeld worden in het configuratiebestand web.config, maar je hoeft dat bestand nooit aan te
raken. Via de Web Site Administration Tool, een administratieve web pagina, kan de beveiliging tot in de puntjes
geregeld worden. Je kunt daarbij gebruik maken van Windows beveiliging, Passport beveiliging, en de ingebouwde
beveiliging op basis van een database. Als je wilt kun je ook zelf een mechanisme implementeren. Zolang je daarbij de
richtlijnen van de zogenaamde Membership API volgt kun je deze via de configuratie instellen zonder dat daarvoor ook
maar een regel code in de applicatie zelf aangepast hoeft te worden. Dit is omdat de implementatie van de Membership
API losgekoppeld is via het Provider Model Design Pattern. De code in de applicatie maakt gebruik van de Membership
API, en is niet afhankelijk van de onderliggende implementatie. Dit biedt allerlei mogelijkheden voor derden om een
custom oplossing te maken die voor iedereen werkt.
Meer beveiliging
Dit artikel is uiteraard maar een vluchtige blik op wat .NET 2.0 allemaal te bieden heeft op het gebied van
beveiliging. Wie meer wil weten zal de beschikbare documentatie in moeten duiken. Een goed startpunt is altijd
http://msdn.microsoft.com/security maar wie echt al
in de (pre-release) documentatie van .NET 2.0 wil snuffelen kan beter kijken op //msdn2.microsoft.com/library/9w6t6kwc(en-us,vs.80).aspx.
Verder zijn er voor ASP.NET al meerdere tutorials te vinden die een overzicht geven van de beveiliging en laten zien
hoe het stap voor stap werkt. Je kunt deze tutorials vinden op http://beta.asp.net/QUICKSTART/aspnet/doc/security
Dit artikel is eerder verschenen in NetOpus, september 2005.
|