Tamper-Evident Configuration Files in ASP.NET

A couple weeks ago someone sent a message to one of our internal mailing lists. His message was pretty straightforward: how do you prevent modifications to a configuration file for an application [while the user has administrative rights on the machine]?

There were a couple responses including mine, which was to cryptographically sign the configuration file with an asymmetric key. For a primer on digital signing, take a look here. Asymmetric signing is one possible way of signing a file. By signing it this way the configuration file could be signed by an administrator before deploying the application, and all the application needed to validate the signature was the public key associated with the private key used to sign the file. This separated the private key from the application, preventing the configuration from being re-signed maliciously. It’s similar in theory to how code-signing works.

In the event that validation of the configuration file failed, the application would not load, or would gracefully fail and exit the next time the file was checked (or the application had an exclusive lock on the configuration file so it couldn’t be edited while running).

We are also saved the problem of figuring out the signature format because there is a well-respected XML signature schema: http://www.w3.org/2000/09/xmldsig#. WCF uses this format to sign messages. For a good code-walkthrough see Barry Dorrans’ Beginning ASP.NET Security. More on the code later here though.

Technically, this won’t prevent changes to the file, but it will prevent the application from accepting those changes. It’s kind of like those tamper-evident tags manufacturers stick on the enclosures of their equipment. It doesn’t prevent someone from opening the thing, but they will get caught if someone checks it. You’ll notice I didn’t call them “tamper-resistance” tags.

Given this problem, I went one step further and asked myself: how would I do this with a web application? A well-informed ASP.NET developer might suggest using aspnet_regiis to encrypt the configuration file. Encrypting the configuration does protect against certain things, like being able to read configuration data. However, there are a couple problems with this.

  • If I’m an administrator on that server I can easily decrypt the file by calling aspnet_regiis
  • If I’ve found a way to exploit the site, I can potentially overwrite the contents of the file and make the application behave differently
  • The encryption/decryption keys need to be shared in web farms

Consider our goal. We want to prevent a user with administrative privileges from modifying the configuration. Encryption does not help us in this case.  Signing the configuration will help though (As an aside, for more protection you encrypt the file then sign it, but that’s out of the scope of this) because the web application will stop working if a change is made that invalidates the signature.

Of course, there’s one little problem. You can’t stick the signature in the configuration file, because ASP.NET will b-itch complain about the foreign XML tag. The original application in question was assumed to have a custom XML file for it’s configuration, but in reality it doesn’t, so this problem applies there too.

There are three possible solutions to this:

  • Create a custom ConfigurationSection class for the signature
  • Create a custom configuration file and handler, and intercept all calls to web.config
  • Stick the signature of the configuration file into a different file

The first option isn’t a bad idea, but I really didn’t want to muck about with the configuration classes. The second option is, well, pretty much a stupid idea in almost all cases, and I’m not entirely sure you can even intercept all calls to the configuration classes.

I went with option three.

The other file has two important parts: the signature of the web.config file, and a signature for itself. This second signature prevents someone from modifying the signature for the web.config file. Our code becomes a bit more complicated because now we need to validate both signatures.

This makes us ask the question, where is the validation handled? It needs to happen early enough in the request lifecycle, so I decided to stick it into a HTTP Module, for the sake of modularity.

Hold it, you say. If the code is in a HTTP Module, then it needs to be added to the web.config. If you are adding it to the web.config, and protecting the web.config by this module, then removing said module from the web.config will prevent the validation from occurring.

Yep.

There are two ways around this:

  • Add the validation call into Global.asax
  • Hard code the addition of the HTTP Module

It’s very rare that I take the easy approach, so I’ve decided to hard code the addition of the HTTP Module, because sticking the code into a module is cleaner.

In older versions of ASP.NET you had to make some pretty ugly hacks to get the module in because it needs to happen very early in startup of the web application. With ASP.NET 4.0, an assembly attribute was added that allowed you to call code almost immediately after startup:

[assembly: PreApplicationStartMethod(typeof(Syfuhs.Security.Web.Startup), "Go")]

 

Within the Startup class there is a public static method called Go(). This method calls the Register() within an instance of my HttpModule. This module inherits from an abstract class called DynamicallyLoadedHttpModule, which inherits from IHttpModule. This class looks like:

public abstract class DynamicallyLoadedHttpModule : IHttpModule
{
    public void Register()
    {
        DynamicHttpApplication.RegisterModule(delegate(HttpApplication app) { return this; });
    }

    public abstract void Init(HttpApplication context);

    public abstract void Dispose();
}

 

The DynamicHttpApplication class inherits from HttpApplication and allows you to load HTTP modules in code. This code was not written by me. It was originally written by Nikhil Kothari:

using HttpModuleFactory = System.Func<System.Web.HttpApplication, System.Web.IHttpModule>;

public abstract class DynamicHttpApplication : HttpApplication
{
    private static readonly Collection<HttpModuleFactory> Factories 
= new Collection<HttpModuleFactory>(); private static object _sync = new object(); private static bool IsInitialized = false; private List<IHttpModule> modules; public override void Init() { base.Init(); if (Factories.Count == 0) return; List<IHttpModule> dynamicModules = new List<IHttpModule>(); lock (_sync) { if (Factories.Count == 0) return; foreach (HttpModuleFactory factory in Factories) { IHttpModule m = factory(this); if (m != null) { m.Init(this); dynamicModules.Add(m); } } } if (dynamicModules.Count != 0) modules = dynamicModules; IsInitialized = true; } public static void RegisterModule(HttpModuleFactory factory) { if (IsInitialized) throw new InvalidOperationException(Exceptions.CannotRegisterModuleLate); if (factory == null) throw new ArgumentNullException("factory"); Factories.Add(factory); } public override void Dispose() { if (modules != null) modules.ForEach(m => m.Dispose()); modules = null; base.Dispose(); GC.SuppressFinalize(this); }

 

Finally, to get this all wired up we modify the Global.asax to inherit from DynamicHttpApplication:

public class Global : DynamicHttpApplication

{ … }

Like I said, you could just add the validation code into Global (but where’s the fun in that?)…

So, now that we’ve made it possible to add the HTTP Module, lets actually look at the module:

public sealed class SignedConfigurationHttpModule : DynamicallyLoadedHttpModule
{
    public override void Init(HttpApplication context)
    {
        if (context == null)
            throw new ArgumentNullException("context");

        context.BeginRequest += new EventHandler(context_BeginRequest);
        context.Error += new EventHandler(context_Error);
    }

    private void context_BeginRequest(object sender, EventArgs e)
    {
        HttpApplication app = (HttpApplication)sender;

        SignatureValidator validator 
= new SignatureValidator(app.Request.PhysicalApplicationPath); validator.ValidateConfigurationSignatures(
CertificateLocator.LocateSigningCertificate()); } private void context_Error(object sender, EventArgs e) { HttpApplication app = (HttpApplication)sender; foreach (var exception in app.Context.AllErrors) { if (exception is XmlSignatureValidationFailedException) { // Maybe do something // Or don't... break; } } } public override void Dispose() { } }

 

Nothing special here. Just hooking into the context.BeginRequest event so validation occurs on each request. There would be some performance impact as a result.

The core validation is contained within the SignatureValidator class, and there is a public method that we call to validate the signature file, ValidateConfigurationSignatures(…). This method accepts an X509Certificate2 to compare the signature against.

The specification for the schema we are using for the signature will actually encode the public key of the private key into the signature element, however we want to go one step further and make sure it’s signed by a particular certificate. This will prevent someone from modifying the configuration file, and re-signing it with a different private key. Validation of the signature is not enough; we need to make sure it’s signed by someone we trust.

The validator first validates the schema of the signature file. Is the XML well formed? Does the signature file conform to a schema we defined (the schema is defined in a Constants class)? Following that is validates the signature of the file itself. Has the file been tampered with? Following that it validates the signature of the web.config file. Has the web.config file been tampered with?

Before it can do all of this though, it needs to check to see if the signature file exists. The variable passed into the constructor is the physical path of the web application. The validator knows that the signature file should be in the App_Data folder within the root. This file needs to be here because the folder by default will not let you access anything in it, and we don’t want anyone downloading the file. The path is also hardcoded specifically so changes to the configuration cannot bypass the signature file validation.

Here is the validator:

internal sealed class SignatureValidator
{
    public SignatureValidator(string physicalApplicationPath)
    {
        this.physicalApplicationPath = physicalApplicationPath;
        this.signatureFilePath = Path.Combine(this.physicalApplicationPath, 
"App_Data\\Signature.xml"); } private string physicalApplicationPath; private string signatureFilePath; public void ValidateConfigurationSignatures(X509Certificate2 cert) { Permissions.DemandFilePermission(FileIOPermissionAccess.Read, this.signatureFilePath); if (cert == null) throw new ArgumentNullException("cert"); if (cert.HasPrivateKey) throw new SecurityException(Exceptions.ValidationCertificateHasPrivateKey); if (!File.Exists(signatureFilePath)) throw new SecurityException(Exceptions.CouldNotLoadSignatureFile); XmlDocument doc = new XmlDocument() { PreserveWhitespace = true }; doc.Load(signatureFilePath); ValidateXmlSchema(doc); CheckForUnsignedConfig(doc); if (!X509CertificateCompare.Compare(cert, ValidateSignature(doc))) throw new XmlSignatureValidationFailedException(
Exceptions.SignatureFileNotSignedByExpectedCertificate); List<XmlSignature> signatures = ParseSignatures(doc); ValidateSignatures(signatures, cert); } private void CheckForUnsignedConfig(XmlDocument doc) { List<string> signedFiles = new List<string>(); foreach (XmlElement file in doc.GetElementsByTagName("File")) { string fileName = Path.Combine(this.physicalApplicationPath,
file["FileName"].InnerText); signedFiles.Add(fileName.ToUpperInvariant()); } CheckConfigFiles(signedFiles); } private void CheckConfigFiles(List<string> signedFiles) { foreach (string file in Directory.EnumerateFiles(
this.physicalApplicationPath, "*.config", SearchOption.AllDirectories)) { string path = Path.Combine(this.physicalApplicationPath, file); if (!signedFiles.Contains(path.ToUpperInvariant())) throw new XmlSignatureValidationFailedException(
string.Format(CultureInfo.CurrentCulture, Exceptions.ConfigurationFileWithoutSignature, path)); } } private void ValidateXmlSchema(XmlDocument doc) { using (StringReader fileReader = new StringReader(Constants.SignatureFileSchema)) using (StringReader signatureReader = new StringReader(Constants.SignatureSchema)) { XmlSchema fileSchema = XmlSchema.Read(fileReader, null); XmlSchema signatureSchema = XmlSchema.Read(signatureReader, null); doc.Schemas.Add(fileSchema); doc.Schemas.Add(signatureSchema); doc.Validate(Schemas_ValidationEventHandler); } } void Schemas_ValidationEventHandler(object sender, ValidationEventArgs e) { throw new XmlSignatureValidationFailedException(Exceptions.InvalidSchema, e.Exception); } public static X509Certificate2 ValidateSignature(XmlDocument xml) { if (xml == null) throw new ArgumentNullException("xml"); XmlElement signature = ExtractSignature(xml.DocumentElement); return ValidateSignature(xml, signature); } public static X509Certificate2 ValidateSignature(XmlDocument doc, XmlElement signature) { if (doc == null) throw new ArgumentNullException("doc"); if (signature == null) throw new ArgumentNullException("signature"); X509Certificate2 signingCert = null; SignedXml signed = new SignedXml(doc); signed.LoadXml(signature); foreach (KeyInfoClause clause in signed.KeyInfo) { KeyInfoX509Data key = clause as KeyInfoX509Data; if (key == null || key.Certificates.Count != 1) continue; signingCert = (X509Certificate2)key.Certificates[0]; } if (signingCert == null) throw new CryptographicException(Exceptions.SigningKeyNotFound); if (!signed.CheckSignature()) throw new CryptographicException(Exceptions.SignatureValidationFailed); return signingCert; } private static void ValidateSignatures(List<XmlSignature> signatures, X509Certificate2 cert) { foreach (XmlSignature signature in signatures) { X509Certificate2 signingCert
= ValidateSignature(signature.Document, signature.Signature); if (!X509CertificateCompare.Compare(cert, signingCert)) throw new XmlSignatureValidationFailedException( string.Format(CultureInfo.CurrentCulture,
Exceptions.SignatureForFileNotSignedByExpectedCertificate, signature.FileName)); } } private List<XmlSignature> ParseSignatures(XmlDocument doc) { List<XmlSignature> signatures = new List<XmlSignature>(); foreach (XmlElement file in doc.GetElementsByTagName("File")) { string fileName
= Path.Combine(this.physicalApplicationPath, file["FileName"].InnerText); Permissions.DemandFilePermission(FileIOPermissionAccess.Read, fileName); if (!File.Exists(fileName)) throw new FileNotFoundException(
string.Format(CultureInfo.CurrentCulture, Exceptions.FileNotFound, fileName)); XmlDocument fileDoc = new XmlDocument() { PreserveWhitespace = true }; fileDoc.Load(fileName); XmlElement sig = file["FileSignature"] as XmlElement; signatures.Add(new XmlSignature() { FileName = fileName, Document = fileDoc, Signature = ExtractSignature(sig) }); } return signatures; } private static XmlElement ExtractSignature(XmlElement xml) { XmlNodeList xmlSignatureNode = xml.GetElementsByTagName("Signature"); if (xmlSignatureNode.Count <= 0) throw new CryptographicException(Exceptions.SignatureNotFound); return xmlSignatureNode[xmlSignatureNode.Count - 1] as XmlElement; } }

 

You’ll notice there is a bit of functionality I didn’t mention. Checking that the web.config file hasn’t been modified isn’t enough. We also need to check if any *other* configuration file has been modified. It’s no good if you leave the root configuration file alone, but modify the <authorization> tag within the administration folder to allow anonymous access, right?

So there is code looks through the site for any files that have the “config” extension, and if that file isn’t in the signature file, it throws an exception.

There is also a check done at the very beginning of the validation. If you pass an X509Certificate2 with a private key it will throw an exception. This is absolutely by design. You sign the file with the private key. You validate with the public key. If the private key is present during validation that means you are not separating the keys, and all of this has been a huge waste of time because the private key is not protected. Oops.

Finally, it’s important to know how to sign the files. I’m not a fan of generating XML properly, partially because I’m lazy and partially because it’s a pain to do, so mind the StringBuilder:

public sealed class XmlSigner
{
    public XmlSigner(string appPath)
    {
        this.physicalApplicationPath = appPath;
    }

    string physicalApplicationPath;

    public XmlDocument SignFiles(string[] paths, X509Certificate2 cert)
    {
        if (paths == null || paths.Length == 0)
            throw new ArgumentNullException("paths");

        if (cert == null || !cert.HasPrivateKey)
            throw new ArgumentNullException("cert");

        XmlDocument doc = new XmlDocument() { PreserveWhitespace = true };
        StringBuilder sb = new StringBuilder();

        sb.Append("<Configuration>");
        sb.Append("<Files>");

        foreach (string p in paths)
        {
            sb.Append("<File>");

            sb.AppendFormat("<FileName>{0}</FileName>", 
p.Replace(this.physicalApplicationPath, "")); sb.AppendFormat("<FileSignature><Signature xmlns=\"http://www.w3.org/2000/09/xmldsig#\">{0}</Signature></FileSignature>",
SignFile(p, cert).InnerXml); sb.Append("</File>"); } sb.Append("</Files>"); sb.Append("</Configuration>"); doc.LoadXml(sb.ToString()); doc.DocumentElement.AppendChild(doc.ImportNode(SignXmlDocument(doc, cert), true)); return doc; } public static XmlElement SignFile(string path, X509Certificate2 cert) { if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullException("path"); if (cert == null || !cert.HasPrivateKey) throw new ArgumentException(Exceptions.CertificateDoesNotContainPrivateKey); Permissions.DemandFilePermission(FileIOPermissionAccess.Read, path); XmlDocument doc = new XmlDocument(); doc.PreserveWhitespace = true; doc.Load(path); return SignXmlDocument(doc, cert); } public static XmlElement SignXmlDocument(XmlDocument doc, X509Certificate2 cert) { if (doc == null) throw new ArgumentNullException("doc"); if (cert == null || !cert.HasPrivateKey) throw new ArgumentException(Exceptions.CertificateDoesNotContainPrivateKey); SignedXml signed = new SignedXml(doc) { SigningKey = cert.PrivateKey }; Reference reference = new Reference() { Uri = "" }; XmlDsigC14NTransform transform = new XmlDsigC14NTransform(); reference.AddTransform(transform); XmlDsigEnvelopedSignatureTransform envelope = new XmlDsigEnvelopedSignatureTransform(); reference.AddTransform(envelope); signed.AddReference(reference); KeyInfo keyInfo = new KeyInfo(); keyInfo.AddClause(new KeyInfoX509Data(cert)); signed.KeyInfo = keyInfo; signed.ComputeSignature(); XmlElement xmlSignature = signed.GetXml(); return xmlSignature; } }

 

To write this to a file you can call it like this:

XmlWriter writer = XmlWriter.Create(
@"C:\Dev\Projects\Syfuhs.Security.Web\Syfuhs.Security.Web.WebTest\App_Data\Signature.xml"); XmlSigner signer = new XmlSigner(Request.PhysicalApplicationPath); XmlDocument xml = signer.SignFiles(new string[] { @"C:\Dev\Projects\Syfuhs.Security.Web\Syfuhs.Security.Web.WebTest\Web.config", @"C:\Dev\Projects\Syfuhs.Security.Web\Syfuhs.Security.Web.WebTest\Web.debug.config", @"C:\Dev\Projects\Syfuhs.Security.Web\Syfuhs.Security.Web.WebTest\Web.release.config", @"C:\Dev\Projects\Syfuhs.Security.Web\Syfuhs.Security.Web.WebTest\Account\Web.config", @"C:\Dev\Projects\Syfuhs.Security.Web\Syfuhs.Security.Web.WebTest\test.config" }, new X509Certificate2(
@"C:\Dev\Projects\Syfuhs.Security.Web\Syfuhs.Security.Web.WebTest\cert.pfx", "1")); xml.WriteTo(writer); writer.Flush();

 

Now within this code, you have to pass in a X509Certificate2 with a private key, otherwise you can’t sign the files.

These processes should occur on different machines. The private key should never be on the server hosting the site. The basic steps for deployment would go something like:

1. Compile web application.
2. Configure site and configuration files on staging server.
3. Run application that signs the configuration and generates the signature file.
4. Drop the signature.xml file into the App_Data folder.
5. Deploy configured and signed application to production.

There is one final note (I think I’ve made that note a few times by now…) and that is the CertificateLocator class. At the moment it just returns a X509Certificate2 from a particular path on my file system. This isn’t necessarily the best approach because it may be possible to overwrite that file. You should store that certificate in a safe place, and make a secure call to get it. For instance a web service call might make sense. If you have a Hardware Security Module (HSM) to store secret bits in, even better.

Concluding Bits

What have we accomplished by signing our configuration files? We add a degree of trust that our application hasn’t been compromised. In the event that the configuration has been modified, the application stops working. This could be from malicious intent, or careless administrators. This is a great way to prevent one-off changes to configuration files in web farms. It is also a great way to prevent customers from mucking up the configuration file you’ve deployed with your application.

This solution was designed in a way mitigate quite a few attacks. An attacker cannot modify configuration files. An attacker cannot modify the signature file. An attacker cannot view the signature file. An attacker cannot remove the signature file. An attacker cannot remove the HTTP Module that validates the signature without changing the underlying code. An attacker cannot change the underlying code because it’s been compiled before being deployed.

Is it necessary to use on every deployment? No, probably not.

Does it go a little overboard with regard to complexity? Yeah, a little.

Does it protect against a real problem? Absolutely.

Unfortunately it also requires full trust.

Overall it’s a fairly robust solution and shows how you can mitigate certain types of risks seen in the real world.

And of course, it works with both WebForms and MVC.

You can download the full source here.

Making the X509Store more Friendly

When you need to grab a certificate out of a Windows Certificate Store, you can use a class called X509Store.  It's very simple to use:

X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);

store.Open(OpenFlags.ReadOnly);

X509Certificate2Collection myCerts = store.Certificates.Find(X509FindType.FindByThumbprint, "...", false);

store.Close();

However, I don't like this open/close mechanism.  It reminds me too much of Dispose(), except I can't use a using statement.  There are lots of arguments around whether a using statement is a good way of doing things and I'm in the camp of yes, it is.  When they are used properly they make code a lot more logical.  It creates a scope for an object explicitly.  I want to do something like this:

using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser, OpenFlags.ReadOnly))
{
    X509Certificate2Collection myCerts = store.Certificates.Find(X509FindType.FindByThumbprint, "...", false);
}

The simple solution would be to subclass this, implement IDisposable, and overwrite some of the internals.  The problem though is that someone on the .NET team thought it would be wise to seal the class.  Crap.  Okay, lets create a new class:

public class X509Store2 : IDisposable
{
    private X509Store store;

    public X509Store2(IntPtr storeHandle, OpenFlags flags)
    {
        store = new X509Store(storeHandle);
        store.Open(flags);
    }

    public X509Store2(StoreLocation storeLocation, OpenFlags flags)
    {
        store = new X509Store(storeLocation);
        store.Open(flags);
    }

    public X509Store2(StoreName storeName, OpenFlags flags)
    {
        store = new X509Store(storeName);
        store.Open(flags);
    }

    public X509Store2(string storeName, OpenFlags flags)
    {
        store = new X509Store(storeName);
        store.Open(flags);
    }

    public X509Store2(StoreName storeName, StoreLocation storeLocation, OpenFlags flags)
    {
        store = new X509Store(storeName, storeLocation);
        store.Open(flags);
    }

    public X509Store2(string storeName, StoreLocation storeLocation, OpenFlags flags)
    {
        store = new X509Store(storeName, storeLocation);
        store.Open(flags);
    }

    public X509Certificate2Collection Certificates { get { return store.Certificates; } }

    public StoreLocation Location { get { return store.Location; } }

    public string Name { get { return store.Name; } }

    public IntPtr StoreHandle { get { return store.StoreHandle; } }

    public void Add(X509Certificate2 certificate)
    {
        store.Add(certificate);
    }

    public void AddRange(X509Certificate2Collection certificates)
    {
        store.AddRange(certificates);
    }

    private void Close()
    {
        store.Close();
    }

    private void Open(OpenFlags flags)
    {
        store.Open(flags);
    }

    public void Remove(X509Certificate2 certificate)
    {
        store.Remove(certificate);
    }
    public void RemoveRange(X509Certificate2Collection certificates)
    {
        store.RemoveRange(certificates);
    }

    public void Dispose()
    {
        this.Close();
    }
}

At this point I've copied all the public members of the X509Store class and called their counterparts in the store.  I've also set Open() and Close() to private so they can't be called.  In theory I could just remove them, but I didn't.

Enjoy!

Claims Transformation and Custom Attribute Stores in Active Directory Federation Services 2

Active Directory Federation Services 2 has an amazing amount of power when it comes to claims transformation.  To understand how it works lets take a look at a set of claims rules and the flow of data from ADFS to the Relying Party:

image

We can have multiple rules to transform claims, and each one takes precedence via an Order:

image

In the case above, Transform Rule 2 transformed the claims that Rule 1 requested from the attribute store, which in this case was Active Directory.  This becomes extremely useful because there are times when some of the data you need to pull out of Active Directory isn’t in a useable format.  There are a couple options to fix this:

  • Make the receiving application deal with it
  • Modify it before sending it off
  • Ignore it

Lets take a look at the second option (imagine an entire blog post on just ignoring it…).  ADFS allows us to transform claims before they are sent off in the token by way of the Claims Rule Language.  It follows the pattern: "If a set of conditions is true, issue one or more claims."  As such, it’s a big Boolean system.  Syntactically, it’s pretty straightforward.

To issue a claim by implicitly passing true:

=> issue(Type = "http://MyAwesomeUri/claims/AwesomeRole", Value = "Awesome Employee");

What that did was ignored the fact that there weren’t any conditions and will always pass a value.

To check a condition:

c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", Value == "SomeRole"]
    => issue(Type = "http://MyAwesomeUri/claims/AwesomeRole", Value = "AwesomeRole");

Breaking down the query, we are checking that a claim created in a previous step has a specific type; in this case role and the claim’s value is SomeRole.  Based on that we are going to append a new claim to the outgoing list with a new type and a new value.

That’s pretty useful in it’s own right, but ADFS can actually go even further by allowing you to pull data out of custom data stores by way of Custom Attribute Stores.  There are four options to choose from when getting data:

  1. Active Directory (default)
  2. LDAP (Any directory that you can query via LDAP)
  3. SQL Server (awesome)
  4. Custom built store via custom .NET assembly

Let’s get some data from a SQL database.  First we need to create the attribute store.  Go to Trust Relationships/Attribute Stores in the ADFS MMC Console (or you could also use PowerShell):

image

Then add an attribute store:

image

All you need is a connection string to the database in question:

image

The next step is to create the query to pull data from the database.  It’s surprisingly straightforward.  This is a bit of a contrived example, but lets grab the certificate name and the certificate hash from a database table where the certificate name is equal to the value of the http://MyCertUri/UserCertName claim type:

c:[type == http://MyCertUri/UserCertName]
   => issue(store = "MyAttributeStore",
         types = ("http://test/CertName", "http://test/CertHash"),
         query = "SELECT CertificateName, CertificateHash FROM UserCertificates WHERE CertificateName='{0}'", param = c.value);

For each column you request in the SQL query, you need a claim type as well.  Also, unlike most SQL queries, to use parameters we need to use a format similar to String.Format instead of using @MyVariable syntaxes.

In a nutshell this is how you deal with claims transformation.  For a more in depth article on how to do this check out TechNet: http://technet.microsoft.com/en-us/library/dd807118(WS.10).aspx.

Certificates and ADFS 2.0

One of the problems with pushing all this data back and forth between Token Services and clients and Relying Parties is that some of this information really needs to encrypted.  If someone can eavesdrop on your communications and catch your token authorization they could easily impersonate you.  We don’t want that.  As such, we use certificates in ADFS for EVERYTHING.

The problem with doing things this way is that certificates are a pain in the neck.  With ADFS we need at least three certificates for each server:

  • Service Communication certificate: This certificate is used for SSL communications for web services and connections between proxies.  This is the certificate used by IIS for the Federation Service site.
  • Token Signing certificate: This certificate is used to sign all tokens passed to the client.
  • Token decryption certificate: This certificate is used for decrypting incoming tokens.  This would be the private key for the Service Communication certificate.

Managing these certificates isn’t easy.  Sometimes we can get away with just using our domain CA, other times we need 3rd party CA’s to sign them.  Microsoft has provided lots of guidance in this, but it’s not the easiest to find.  You can access it on TechNet (Certificate Requirements for Federation Servers): http://technet.microsoft.com/en-ca/library/dd807040(WS.10).aspx

Hopefully that helps.

Working with Certificates in Code

Just a quick little collection of useful code snippets when dealing with certificates.  Some of these don’t really need to be in their own methods but it helps for clarification.

Namespaces for Everything

using System.Security.Cryptography.X509Certificates;
using System.Security;

Save Certificate to Store

// Nothing fancy here.  Just a helper method to parse strings.
private StoreName parseStoreName(string name)
{
    return (StoreName)Enum.Parse(typeof(StoreName), name);
}
	
// Same here
private StoreLocation parseStoreLocation(string location)
{
    return (StoreLocation)Enum.Parse(typeof(StoreLocation), location);
}
	
private void saveCertToStore(X509Certificate2 x509Certificate2, StoreName storeName, StoreLocation storeLocation)
{
    X509Store store = new X509Store(storeName, storeLocation);

    store.Open(OpenFlags.ReadWrite);
    store.Add(x509Certificate2);

    store.Close();
}

Create Certificate from byte[] array

private X509Certificate2 CreateCertificateFromByteArray(byte[] certFile)
{
     return new X509Certificate2(certFile); 
	// will throw exception if certificate has private key
}

The comment says that it will throw an exception if the certificate has a private key because the private key has a password associated with it. If you don't pass the password as a parameter it will throw a System.Security.Cryptography.CryptographicException exception.

Get Certificate from Store by Thumbprint

private bool FindCertInStore(
    string thumbprint, 
    StoreName storeName, 
    StoreLocation storeLocation, 
    out X509Certificate2 theCert)
{
    theCert = null;
    X509Store store = new X509Store(storeName, storeLocation);

    try
    {
        store.Open(OpenFlags.ReadWrite);

        string thumbprintFixed = thumbprint.Replace(" ", "").ToUpperInvariant();

        foreach (var cert in store.Certificates)
        {
            if (cert.Thumbprint.ToUpperInvariant().Equals(thumbprintFixed))
            {
                theCert = cert;

                return true;
            }
        }

        return false;
    }
    finally
    {
        store.Close();
    }
}

Have fun!