Tuesday, November 30, 2010

Automated signing of unsigned .NET dlls, and updating references between them

SharePoint is one of many beasts I spend a great deal of time tackling. In this particular case I found myself building a Proof-of-Concept webpart, meant for deployment into SP, around an unsigned API.

Being built on .NET, SharePoint will either require you to deploy dlls into the web application's bin folder - in which case the dlls can be unsigned - or into the global assembly cache. Deploying to the GAC (obviously) requires signed dlls.

Given my set of dlls - 50 of them actually - a couple signed, but most not; my only option would be to deploy them to the application's bin folder. The trouble with this approach, however, is two-fold.

First of all, deploying to the bin folder leaves you in the hand of the .NET CAS (code access security) policies. Granted the default trust settings in SharePoint, the dlls deployed would have very little access to the various system classes, file system and network. The way around this is to define a looser trust model all around - which will defeat the purpose of CAS policies in the first place, or add specific trust levels for all the assemblies. Figuring out the specific policies required for each of the 50 dlls isn't something I'd want to do for a in-development Proof-of-Concept.

The second issue with bin folder deployment, is the fact that even though you'll be able to deploy all the unsigned dlls there, you couldn't reference any of them from the signed dll which holds the SharePoint webpart. The way around that is to dynamically load each required dll with reflection. That, however, would leave you without a single symbol to use directly. All calls would have to be made through reflection. One could construct a signed bootstrapper assembly, which dynamically loades an unsigned dll of your own, that deals with the unsigned dlls - but that quickly turns hairy as well, when the webpart's ui has to call into and retrieve values from the unsigned dlls.

So, for test and development purposes, what I decided to do was write a PowerShell script which:
  • Disassembles all dlls in a folder
  • Parses the IL and updates references between the dlls, in order to point to their newly signed counterparts
  • Assembles and signs all dlls with a supplied key
And that amounts to the following:

param($keyfile = $(throw "Specify keyfile"), $backupFolder = "SignBackup")
function get-publickeytoken($keyfile) {
$tempfile = [io.path]::GetTempFileName()
sn -q -p $keyfile $tempfile
$token = sn -q -t $tempfile
rm $tempfile
$token -replace ".*is ",''
}
function get-ilkeytoken($token) {
"($($token -replace "([A-f0-9]{2})",'$1 '))"
}
$token = get-ilkeytoken(get-publickeytoken $keyfile)
$cd = get-location
$filesToClean = @()
# update il
gci *.dll | %{
$dll = $_
$fn = [system.io.path]::GetFileNameWithoutExtension($dll)
$ilFile = "$fn.il"
$filesToClean += "$ilFile","$fn.res"
ildasm $dll /out:$ilFile >$null
$ilFilePath = "$cd\$fn`.il"
$il = [io.file]::readalltext($ilFilePath)
write-host -ForegroundColor Blue Processing references for $dll
[regex]::matches($IL, "\.assembly extern (.*?)\r\n") | %{
# has external references
$reference = $_.groups[1]
$referencePath = "$cd\$reference`.dll"
if ([io.file]::exists($referencePath)) {
# it's a local reference
if ($il -match ".assembly extern $reference\r\n{[^}]*\r\n\s*\.publickeytoken") {
# already has a strongname
$il = [regex]::Replace($il,
"(extern $reference\r\n{[^}]*)\r\n\s*.publickeytoken\s*=\s*.*?`$([^}]*)",
"`$1`r`n .publickeytoken = $token`$2",
[Text.RegularExpressions.RegexOptions]::Multiline)
write-host -ForegroundColor DarkBlue Updated reference to $reference - Changed PublicKeyToken
}
else {
# has no strong name
$il = [regex]::Replace($il,
"(extern $reference\r\n{)",
"`$1`r`n .publickeytoken = $token",
[Text.RegularExpressions.RegexOptions]::Multiline)
write-host -ForegroundColor DarkBlue Updated reference to $reference - Added PublicKeyToken
}
}
}
set-content -path $ilFilePath $il
}
# rebuild dlls
rmdir -confirm:$false -r -force $backupFolder 2>$null
$null = mkdir $backupFolder
gci *.dll | %{
$dll = $_
$fn = [system.io.path]::GetFileNameWithoutExtension($dll)
$ilFile = "$fn.il"
ilasm $ilFile /res:$fn.res /dll /key:key.snk /out:$fn-signed.dll >$null
$ok = sn -vf $fn-signed.dll
if ($ok -match "is valid") {
mv $dll $backupFolder
mv $fn-signed.dll $dll
write-host -ForegroundColor Green Signed $dll
}
else {
write-host -ForegroundColor Red Failed to sign $dll
}
}
write-host -NoNewline -ForegroundColor Gray Removing temporary files
$filesToClean | %{
write-host -NoNewline -ForegroundColor Gray .
rm $_
}
write-host

And here's a screencast of it running: http://screencast.com/t/Y1t1dcRjN.