Thursday, 19 June 2014

SNK, we need to talk…


Because the world needs another rant about SNK and NuGet


In .NET assemblies, strong names are an optional lightweight signing mechanism that provides identity, including versioning support. Part of the idea here is that a calling assembly can have a pretty good idea that what it asks for is what it gets – it asks for Foo.Bar with key {blah}, version “1.2.3”, and it gets exactly that from the GAC, and the world is rosy. If somebody installs a new additional version of Foo.Bar, or a Foo.Bar from a different author, the old code still gets the dll it wanted, happy in the knowledge that the identity is reliable.
There are only a few problems with this:
  • Most applications don’t use the GAC; the only times people generally “choose” to use the GAC is when their list of options had exactly one option: “use the GAC”. Sharepoint and COM+, I’m looking at you and judging you harshly
  • Actually, there’s a second category of this: people who use strong names because that is what their corporate policy says they must do, with some some well-meaning but completely misguided and incorrect notion that this provides some kind of security. Strong naming is not a security feature. You are just making work and issues for yourself; seriously
  • It doesn’t actually guarantee the version: binding redirect configuration options (just in an xml file in your application) allow for a different version (with the same key) to be provided
  • It doesn’t actually guarantee the integrity of the dll: if somebody has enough access to your computer that they have access to the GAC, they also have enough access to configure .NET to skip assembly identity checking for that dll (just a “snk –Vr {assembly}” away)
And of course, it introduces a range of problems:
  • Versioning becomes a huge pain the backside for all downstream callers, who now need to manage the binding redirect configuration every time a dll gets upgraded anywhere (there are some tools that can help with this, but it isn’t perfect)
  • A strong-named assembly can only reference other strong-named assemblies
  • You now have all sorts of key management issues over your key file (despite the fact that it is pointless and can be bypassed, as already mentioned)

 

Assembly management versus package management


Now enter package management, i.e. NuGet and kin (note: I’m only using NuGet as the example here because it is particularly convenient and readily available to .NET developers; other package management tools exist, each with strengths and weaknesses). Here we have a tool that clearly targets the way 95% of the .NET world actually works:
  • no strong name; we could not care less (or for the Americans: we could care less)
  • no GAC: libraries deployed alongside the application for per-application isolation and deployment convenience (this is especially useful for web-farms, where we just want to robocopy the files out)
  • versioning managed by the package management tool
So, as a library author, it is hugely tempting to simply ignore strong naming, and simply put assemblies without a strong name onto NuGet. And that is exactly what I have done with StackExchange.Redis: it has no strong name. And for most people, that is fine. But now I get repeated calls and emails from people saying “please can you strong name it”.
The argument for and against strong-naming in NuGet is very verbose; there are threads with hundreds of messages for and against – both with valid points. There is no simple answer here.
  • if it is strong named, I introduce the problems already mentioned – when for 95% (number totally invented, note) of the people using it, this is simply not an issue
  • if it isn’t strong named, people doing Sharepoint development, or COM+ development, or just with awkward local policies cannot use it – at least not conveniently
They can of course self-sign locally, and there are tools to help with that – including Nivot.StrongNaming. But this only helps for immediate references: any such change will be fatal to indirect references. You can’t use binding redirects to change the identity of an assembly – except for the version.
I could start signing before deployment, but that would be a breaking change. So I’d have to at a minimum do a major version release. Again, direct references will be fine – just update the package and it works – but indirect references are still completely toast, with no way of fixing them except to recompile the intermediate assembly against the new identity. Not ideal.

I’m torn


In some ways, it is tempting to say “screw it, I need to add a strong name so that the tiny number of people bound by strong naming can use it”, but that is also saying “I need to totally and irreconcilably break all indirect references, to add zero functionality and despite the fact that it was working fine”, and also “I actively want to introduce binding redirect problems for users who currently don’t have any issues whatsoever”.
This is not an easy place to be. Frankly, at this stage I’m also not sure I want to be adding implicit support to the problems that SNK introduce by adding a strong name.

But what if…


Imagineering is fun. Let’s be realistic and suppose that there is nothing we can do to get those systems away from strong names, and that we can’t change the strong-named-can-only-reference-strong-named infection. But let’s also assume we don’t want to keep complicating package management by adding them by default. Why can’t we let this all just work. Or at least, that maybe our package management tools could fix it all. It seems to me that we would only need two things:
  • assembly binding redirects that allow unsigned assemblies to be forwarded to signed assemblies
  • some inbuilt well-known publicly available key that the package management tools could use to self-sign assemblies
For example, imagine that you have your signed application. and you use NuGet to add a package reference to Microsoft.Web.RedisSessionStateProvider, which in turn references StackExchange.Redis. In my imaginary world, NuGet would detect that the local project is signed, and these aren’t – so it would self-sign them with the well-known key, and add an assembly-binding redirect from “StackExchange.Redis” to “StackExchange.Redis with key hash and version”. Important note: the well-known key here is not being used to assert any particular authorship etc – it is simply “this is what we got; make it work”. There’s no need to protect the private key of that.
The major wrinkle in this, of course, is that it would require .NET changes to the fusion loader, in order to allow a binding redirect that doesn’t currently exist. But seriously: haven’t we been having this debate for long enough now? Isn’t it time the .NET framework started helping us with this? If I could request a single vNext CLR feature: this would be it.
Because I am so very tired of having this whole conversation, after a decade of it.
There are probably huge holes in my reasoning here, and reasons why it isn’t a simple thing to change. But: this change, or something like it, is so very very overdue.

And for now…


Do I add a strong name to StackExchange.Redis? Local experiments have shown me that whatever I do: somebody gets screwed, and no matter what I do: I’m going to get yelled at by someone. But I’m open to people’s thoughts and suggestions.

11 comments:

Unknown said...

There is another approach (I'm not saying it's ideal but just mentioning it): maintain two versions of the assembly - signed and unsigned. You can have two NuGet packages and people can choose which one to use.

Maybe NuGet could internally support both signed and unsigned versions in the similar way it supports different platform targets.

Marc Gravell said...

Yeah, I've actually done that before.

The problem there is that most of the more complex issues relate to indirect references. This doesn't help with that.

Logan Capaldo said...

Besides COM+/Sharepoint and misguided policies, another place in my experience where strong names are effectively mandatory (and I realize that this is even _more_ niche) is if you happen to be using the hosting apis (IHostAssemblyStore etc.) to pull assemblies from non-traditional locations. The CLR is very picky about what identities it will even bother to call ProvideAssembly with.

Marc Gravell said...

Noted - thanks

David Poirier said...

I think you're doing the right thing by leaving it unsigned.

If someone desperately needs a signed version for the GAC they can clone the repo, add an SNK and build it. It's more work but if they can't be bothered...

Grumpydev said...

Keep it unsigned - you don't even need to do manual builds to add SN if you really need it, you can make it part of your own app's build process with things like this:

https://github.com/brutaldev/StrongNameSigner

Sean Kearon said...

Is there an existing feature request for CLR vNext that we can vote on?

Kyle Gobel said...

So why can't they just remove the restriction that signed assemblies can only reference other signed assemblies?

I think I'm missing a piece of the puzzle here.

Simon said...

I have been writing this product for almost 10 years now. I's been strong named for all that time. As you pointed you, I don't think you really have the choice here today with .NET as it is. I simply *never* changed that strong name. The version is still 1.0.0.0. For versioning, I use instead the AssemblyInformationalVersion, and I make sure newer versions just don't break older programs.

Marc Gravell said...

@Simon yes, that is basically the same conclusions I had reached over the last however-long of staring at this. See http://blog.marcgravell.com/2014/07/snk-and-me-work-out-compromise.html for more.

Rowland O'Connor said...

Here we go again....

One other compelling use cases that requires assembly signing:

External unit testing of internal members using the "AssemblyVisibleTo" attribute. Personally, I don't want to make everything in an assembly "Public" just to enable good coverage in unit tests.

In more than 10 years of creating .Net assemblies, I have never had issues with signing or consuming signed assemblies.

Whilst the argument on signing or not signing rages on, it seems that in recent months, many Developers publishing Nuget packages are realising the practical advantages of simply just signing the binaries-JFDI !

Having managed development teams over the years, my message on signing is to just sign it. In many cases, not signing is not a conscious choice with developers but rather just them not knowing about signing and the default project set ups in .Net being no SNK.

Imagine where we would all be if the authors of Json.Net, Enterprise Library, Entity Framework etc chose not to sign? Interesting....

Yes, I agree that the world would be a happier and less confusing place if we all standardised on not signing. However, we don't live in an idealistic world but a practical one based on convention. So, practically speaking, signing is the way to go IMHO.

Our development team leaves theses arguments to the academics (their remit), whilst we concentrate on creating software (our remit).

Thanks for the Nuget package BTW. Yes, we are one of the minority using the signed version;)