Thursday, November 3, 2011

Certificate based Authentication and WCF

Certificate based authentication with WCF has two components - configuring credentials and determining trust.

The first part is easy - you simply set the clientCredentialType in the binding's security configuration to Certificate. This means that WCF will demand that the client sends a certificate along with the (first) request - either as a WS-Security X509 token or using SSL client certificates (depending on the security mode this also means that this requirement becomes part of the WSDL/Policy).

Furthermore the "plumbing" (either SSL or WCF message security) will make sure that the incoming certs are technically valid. This includes making sure that the certificate is not malformed and that the signature matches the public key. If this is not the case, the request gets rejected at a very low level and usually your service code would never get invoked.

At this point you have a technically valid certificate - but this does not necessarily mean that you also trust that certificate. The default validation strategy for certificates (regardless of message vs. transport) is called Chain Trust, this means:
• the certificate must be issued from a CA in your trusted CA list (in the machine certificate store)
• this intended purpose of that CA must include "Client Authentication"
• the current date/time must be within the certificate's validitiy period
With message security you also get a mode called Peer Trust. In this mode WCF simply checks if the incoming certificate is installed in the Trusted People folder in the certificate store (the expiration time is checked too).
When to use which mode?

Peer Trust

Since the Trusted People folder must hold all allowed certificates, this means that the service (or the client) has to know every peer a priori. This is OK if you have a limited amount of certificates that you want to allow - but does not scale very well. If you have more than one service on a machine and want to use peer trust, you have to run them under different accounts and use the current user certificate store to separate the "allowed" list.

Chain Trust

Chain trust is for scenarios where you don't know every certificate explicitly, but want to establish trust based on the issuer. The problem you have here is, that by default the trusted CA list is quite, errm, extensive. It includes popular CAs like VeriSign and not so popular ones (which I never heard before). So using chain trust it is quite easy to get a certificate that would be trusted by your service.
So in a lot of cases peer trust is not enough and chain trust is too much. What you typically want to have is first validating the trust chain and afterwards restrict to a specific CA or specific properties in the client cert. How do you accomplish that? Well - it depends ;)

In classic SSL transport security this is accomplished by using a Certificate Trust List (CTL). The moral equivalent in message based security is a X509 Certificate Validator. A third approach (which works in both modes) would be a WCF Service Authorization Manager.
In the next posts I will have a look at these technique and show you the up/down sides of each. Stay tuned.

Certificate based Authentication and WCF (Message Security)
When using message security, the intended way to validate an incoming credential (== token) is a token validator. You can find several internal validators in the System.IdentityModel.Selectors namespace (e.g. for UserName, X.509 or Windows tokens). The X509 token validators gets called whenever an incoming certificate has to be validated - when you have secure conversation enabled, this happens only on the first request which makes this approach very efficient.

WCF has three builtin validators for X.509 certificates and you can choose which one to use via a service/endpoint behavior. I will show the service side settings here, but the same switches also exist on the client side.






revocationMode="Online" />






The certificateValidationMode specifies how incoming certificates are validated and how trust is determined:

None. No validation is performed. Not recommended.
ChainTrust. The certificate has to chain up to one of the CAs in your trusted CA certificate folder.
PeerTrust. The incoming certificate has to be in the Trusted People certificate folder.
PeerOrChainTrust. A combination of Chain and Peer trust.
Custom. For all other cases.

The trustedStoreLocation attribute determines whether the current user or local machine store is used for peer or chain checks - defaults to local machine. Furthermore you specify how revocation lists should get checked via the revocationMode attribute (no check, offline or online).

See this post for a more detailed description of the validation modes. Also, as mentioned in that post, the standard validation modes are mostly useful in niche situations.

This is where the Custom mode comes into play. With this mode it is your responsibility to validate the certificate following your own guidelines. You then make decisions if you want to accept the certificate.

A custom certificate validator involves deriving from X509CertificateValidator and implementing the Validate() method. The WCF plumbing passes the incoming certificate into this method. If you want to reject the certificate you throw a SecurityTokenValidationException inside Validate().

So far so good - now, which steps are involved to validate a certificate? Before you can rely on any information in the cert, you have to make sure it's valid - typcially by checking it is not expired or revoked and is issued by a trusted CA.

Afterwards you can check certain properties of the certificate or its issuer to further restrict the allowed certs. Another approach would be - like PeerTrust - to check the certificate against a list of allowed certificates.

For checking the trust chain, you use the X509Chain class - the general logic of Validate() looks like this:

public override void Validate(X509Certificate2 certificate)
{
// create chain and set validation options
X509Chain chain = new X509Chain();
SetValidationSettings(chain);

// check if cert is valid and chains up to a trusted CA
if (!chain.Build(certificate))
{
throw new SecurityTokenValidationException(
"Client certificate is not valid");
}

// check if cert is from our trusted list
if (!IsTrusted(chain, GetTrustedThumbprints()))
{
throw new SecurityTokenValidationException(
"Client certificate is not trusted");
}
}
How the certificate should be exactly validated can be specified on the ChainPolicy property of X509Chain. Besides the configurable revocation mode, WCF uses the default settings for the chain policy, which are:

protected override void SetValidationSettings(X509Chain chain)
{
chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
chain.ChainPolicy.VerificationTime = DateTime.Now;
chain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 0, 0);
}

My implementation of IsTrusted then checks if either an issuer or a the end certificate itself (specified by the ValidationMode property) is in a trust list. The check is done by comparing the thumbprint of the certificate in question against a list.

protected virtual bool IsTrusted(X509Chain chain, string[] trustedThumbprints)
{
int depth = 0;

if (ValidationMode == ValidationMode.EndCertificate)
{
// only check the end certificate
return CheckThumbprint(chain.ChainElements[0].Certificate, trustedThumbprints);
}
else
{
// check the rest of the chain
foreach (X509ChainElement element in chain.ChainElements)
{
if (++depth == 1)
{
continue;
}

if (CheckThumbprint(element.Certificate, trustedThumbprints))
{
return true;
}
}
}

return false;
}


The last step is to register the validator in a serviceCredentials behavior.


x509FindType="FindBySubjectName"
storeLocation="CurrentUser"
storeName="My" />


customCertificateValidatorType="type" />



In the download you can find a ready to use validator base class from which you can derive from. You just have to implement the GetTrustedThumbprints method and return a string[] of thumbprints. You can get the thumbprints from the certificate UI (just remove the blanks). Have fun!

No comments :

Post a Comment