In a system with well-known end-points, using WS-Addressing and WS-Security with X509 certificates is a great way to authenticate web services, and ensure that the messages are from the right people and have not been tampered with. The basic setup can be implemented very quickly if you're happy with the default way messages are passed, but when you have other requirements such as changing the digest algorithms things can get more tricky. These are the requirements I had to meet:
- Do not pass the X509 certificate in the message
- Apply multiple canonicalizing transforms to each signed message part
- Change the digest and signature algorithms to SHA256 (the default is SHA1 when using X509 certificates)
I managed to do most of this, so let's have a look at each in turn. Note that I was working with WSE2 Service Pack 3 in .NET 1.1 so this information may not be correct for other versions.
Omitting the X509 certificate from the message
The most basic code to sign a request with an X509 certificate is shown below. This gets a certificate from the local machine store, creates a signature from an X509 security token using the default settings, and then adds the token and signature to the request context. The generated SOAP message contains a BinarySecurityToken element with a base-64 encoded representation of the certificate and a Signature block containing the usual information.
//grab a certificate from the machine store - obviously in the real system
//this would locate a specific certificate not just the first one found
X509Certificate certificate;
using (X509CertificateStore store = X509CertificateStore.LocalMachineStore(
X509CertificateStore.MyStore))
{
store.OpenRead();
certificate = store.Certificates[0];
}
//create signature with token
X509SecurityToken token = new X509SecurityToken(certificate);
MessageSignature signature = new MessageSignature(token);
//add the token and signature to the request context
MyWebServiceProxy serviceProxy = new MyWebServiceProxy();
serviceProxy.RequestSoapContext.Security.Elements.Add(signature);
serviceProxy.RequestSoapContext.Security.Tokens.Add(token);
There's no problem with this, but in the system I was developing the requirement was not to pass the certificate in the message but to pass a reference to a (public key only) certificate installed on the target machine to reduce the size of the message. There are a number of ways to do this defined in the WS-Security spec, however a reliable one that is supported natively by WSE2 is to pass the KeyIdentifier of the certificate in the KeyInfo block. To achieve this we don't add the X509SecurityToken to the request context, but instead create a SecurityTokenReference with a key identifier as shown in the method below:
static SecurityTokenReference AddX509KeyIdentifierKeyInfo(
MessageSignature signature,
X509Certificate certificate)
{
//create a keyinfo if it doesn't exist
if (signature.KeyInfo == null)
{
signature.KeyInfo = new KeyInfo();
}
//create and add the token
SecurityTokenReference tokenRef = new SecurityTokenReference();
tokenRef.Id = "KeyInfo-" + Guid.NewGuid().ToString(
"D",
CultureInfo.InvariantCulture);
tokenRef.KeyIdentifier = new KeyIdentifier(
certificate.GetKeyIdentifier(),
KeyIdentifier.ValueTypes.X509SubjectKeyIdentifier);
signature.KeyInfo.AddClause(tokenRef);
return tokenRef;
}
The code to send the message now becomes:
//grab a certificate from the machine store
X509Certificate certificate;
using (X509CertificateStore store = X509CertificateStore.LocalMachineStore(
X509CertificateStore.MyStore))
{
store.OpenRead();
certificate = store.Certificates[0];
}
//create signature with token
X509SecurityToken token = new X509SecurityToken(certificate);
MessageSignature signature = new MessageSignature(token);
//add a key identifier for the token
AddX509KeyIdentifierKeyInfo(signature, certificate);
//add the signature to the request context
MyWebServiceProxy serviceProxy = new MyWebServiceProxy();
serviceProxy.RequestSoapContext.Security.Elements.Add(signature);
When the message is received by the web service it uses this key identifier to find the certificate in its local store. You can control which store it looks in (either local machine or current user) with the WSE2 configuration, and also allow it to accept non-trusted certificates such as those used in testing, with the following configuration information in Web.config.
<configuration>
<configSections>
<section
name="microsoft.web.services2"
type="Microsoft.Web.Services2.Configuration.WebServicesConfiguration,
Microsoft.Web.Services2, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35" />
</configSections>
<microsoft.web.services2>
<security>
<x509 storeLocation="LocalMachine" verifyTrust="true"
allowTestRoot="true" />
</security>
</microsoft.web.services2>
</configuration>
The easiest way to get some certificates to try this out with is to install the certificates that ship with the WSE2 samples, which are found in %ProgramFiles%\Microsoft WSE\v2.0\Samples\Sample Test Certificates.
Applying custom transforms to signed parts
By default the signature applied by WSE2 signs all of the WS-Addressing elements (which it adds automatically), the timestamp, and the message body. It applies the C14N canonicalising transform to each of these before taking the digest. If you want to use a different transform such as C14N with comments, or multiple transforms, then it is possible to override this behaviour and apply your own transform set.
When creating a reference to a custom message part you can specify your own transform chain as follows:
static SignatureReference AddReference(
MessageSignature signature,
string entityId,
TransformChain referenceTransforms)
{
//create the reference - empty references are legal and relate to the
//whole message otherwise we need to prefix it with # to show it is a
//local reference
string referenceId = (entityId != null && entityId.Length > 0) ?
"#" + entityId : string.Empty;
SignatureReference reference = new SignatureReference(referenceId);
//add each of the transforms
if (referenceTransforms != null && referenceTransforms.Count > 0)
{
foreach (Transform transform in referenceTransforms)
{
reference.AddTransform(transform);
}
}
//add the reference to the signature
signature.SignedInfo.AddReference(reference);
return reference;
}
The problem is that if you want to apply your own transforms to a generated part of the message such as the SOAP body then the identifer is unknown as the body doesn't exist until the web method has been called, and the identifier is auto-generated by the WSE2 framework when it signs it.
To get around this we need to look at the method WSE2 uses to generate the message that goes over the wire. Initially it creates a standard SOAP message, and then this is passed through an outgoing 'pipeline' which applies a sequence of 'filters' in order. The security parts of the message are created by the SecurityOutputFilter. A simple workaround I came up with is to use a pipeline filter that sets the body identifier after the message has been generated, but before the security filter sees it. The output filter is as follows:
public class BodyIdOutputFilter : SoapOutputFilter
{
private string bodyId;
public BodyIdOutputFilter(string bodyId)
{
this.bodyId = bodyId;
}
public override void ProcessMessage(SoapEnvelope envelope)
{
if (envelope != null && envelope.Body != null)
{
XmlAttribute bodyIdAttribute = envelope.Body.GetAttributeNode(
WSUtility.AttributeNames.Id,
WSUtility.NamespaceURI);
if (bodyIdAttribute == null)
{
bodyIdAttribute = envelope.Body.SetAttributeNode(
envelope.CreateAttribute(
WSUtility.Prefix,
WSUtility.AttributeNames.Id,
WSUtility.NamespaceURI));
}
bodyIdAttribute.Value = bodyId;
}
}
}
With this filter written it is a trivial matter to create a reference to the body and apply whatever transforms you want, as shown below. An important thing to note is that the signature options have been changed to not automatically include any parts of the message as we will explicitly create references to the parts we want using the custom transform chain. Also note that the AddReference function has been used to add a reference to the KeyIdentifier data so that it cannot be tampered with to change the details of the certificate.
//grab a certificate from the machine store
X509Certificate certificate;
using (X509CertificateStore store = X509CertificateStore.LocalMachineStore(
X509CertificateStore.MyStore))
{
store.OpenRead();
certificate = store.Certificates[0];
}
//create signature with token
X509SecurityToken token = new X509SecurityToken(certificate);
MessageSignature signature = new MessageSignature(token);
signature.SignatureOptions = SignatureOptions.IncludeNone;
//create the transform chain for the references
TransformChain referenceTransforms = new TransformChain();
referenceTransforms.Add(new XmlDsigEnvelopedSignatureTransform());
referenceTransforms.Add(new XmlDsigExcC14NTransform());
//add a key identifier for the token
SecurityTokenReference tokenRef = AddX509KeyIdentifierKeyInfo(
signature,
certificate);
AddReference(signature, tokenRef.Id, referenceTransforms);
//generate a body id and add a reference to it
string bodyId = "Id-" + Guid.NewGuid().ToString(
"D",
CultureInfo.InvariantCulture);
AddReference(signature, bodyId, referenceTransforms);
//add the signature to the request context and add the body is
//filter to the pipeline
MyWebServiceProxy serviceProxy = new MyWebServiceProxy();
serviceProxy.RequestSoapContext.Security.Elements.Add(signature);
serviceProxy.Pipeline.OutputFilters.Add(new BodyIdOutputFilter(bodyId));
Changing digest and signature algorithms
When using an X509SecurityToken to sign a message the digest and signature algorithms default to SHA1. For additional security we wanted to be able to change this to a longer key-length algorithm such as SHA256 or even SHA512. For the digest algorithm it seemed like this should be simple as the DigestMethod property of a SignatureReference is read/write and so you should be able to specify any algorithm, as in the following modification to the AddReference function:
static SignatureReference AddReference(
MessageSignature signature,
string entityId,
TransformChain referenceTransforms)
{
//create the reference - empty references are legal and relate to the
//whole message otherwise we need to prefix it with # to show it is a
//local reference
string referenceId = (entityId != null && entityId.Length > 0) ?
"#" + entityId : string.Empty;
SignatureReference reference = new SignatureReference(referenceId);
//add each of the transforms
if (referenceTransforms != null && referenceTransforms.Count > 0)
{
foreach (Transform transform in referenceTransforms)
{
reference.AddTransform(transform);
}
}
//change to our preferred sha256 digest algorithm - note that this
//would probably come from a configuration file in a real system
reference.DigestMethod = "http://www.w3.org/2001/04/xmlenc#sha256";
//add the reference to the signature
signature.SignedInfo.AddReference(reference);
return reference;
}
The problem here is that underneath WSE2 creates the digest algorithm using the .NET framework's CryptoConfig.CreateFromName(), passing in the supplied string, and .NET 1.1 does not recognise the canonical form of the SHA256 algorithm name so it will fail. Fortunately the behaviour of this method can be modified by adding a section to Machine.config (it does not work in local configuration files) which lets you map any name to any algorithm. Adding the following to both the client and the server allows the above code to be used to transmit messages with a SHA256 digest:
<mscorlib>
<cryptographySettings>
<cryptoNameMapping>
<cryptoClasses>
<cryptoClass sha256="System.Security.Cryptography.SHA256Managed,
mscorlib, Version=1.0.5000.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089, Custom=null" />
</cryptoClasses>
<nameEntry name="http://www.w3.org/2001/04/xmlenc#sha256"
class="sha256" />
</cryptoNameMapping>
</cryptographySettings>
</mscorlib>
Finally, I said at the start that I achieved most of the specification but not all... the one bit that caused a real problem was changing the signature algorithm. After much digging around in the WSE2 code using Lutz Roeder's excellent .NET Reflector it became apparent that when a SecurityToken is specified for the signature, the signature value is computed using the SignatureFormatter specified by that token, which in the case of X509SecurityToken is hard-coded to be an RSASHA1SignatureFormatter.
I had a look into the feasibility of writing a SHA256 version of the formatter and then either subclassing or rewriting parts of the WSE2 framework to fit it in, and did actually make some progress along this route as SHA256 is supported in the base .NET framework so all the calls can be forwarded on, but it ended up being pretty complex and really didn't seem like something that was intended to be done with the WSE2 model. Maybe it's easier in WSE3... I'm sure I'll find out when we move the project to .NET 2.0!
Posted
Jan 14 2006, 12:04 PM
by
Greg Beech