Freigeben über


Custom Policy Assertions in WSE 3 - Part 4

In part 3, I showed how to use WSE's builtin assertions and a new custom assertion to build an election system that meets the 4 requirements laid out in part 1.  In this part I'll show how to implement the blind signature assertion.

 

A good place to start is How to: Create a Custom Policy Assertion in the WSE documentation. This assertion doesn't secure the message (i.e. it doesn't make use of the Microsoft.Web.Services3.Security.Security class), so the section "To create a custom policy assertion that does not secure SOAP messages" is where to begin. The documentation builds it from the bottom up, starting with the SoapFilters where any message transformation or verification takes place, to the PolicyAssertion which ties the SoapFilters together into a policy, to the ReadXml method on PolicyAssertion that given some xml, initializes a PolicyAssertion. Since I've already shown the xml, I'll go the other direction starting with the PolicyAssertion and ending with the SoapFilters.

 

The BlindSignatureAssertion is designed to be application agnostic, so theoretically it can be dropped into a completely different application without modification. To allow this the application must implement several interfaces, we encountered the first in part 3 with the signatureMessageConverter policy element, I'll point the rest out when we come to them.

 

BlindSignatureAssertion

As implemented, this class only has a default constructor so it is necessary to use policy xml to initialize it. If an assertion is expected to be created directly in code a constructor should be provided. The heart of this class is the ReadXml method:

 

public override void ReadXml(System.Xml.XmlReader reader, IDictionary<string, Type> extensions)
{
if (reader == null) throw new ArgumentNullException("reader");
if (extensions == null) throw new ArgumentNullException("extensions");

 

      bool isEmpty = reader.IsEmptyElement;
reader.ReadStartElement();

 

      if (!isEmpty)
{
while (reader.MoveToContent() == XmlNodeType.Element)
{
switch (reader.Name)
{
case "x509":

                      ...
break;
case "signatureProtection":
....

                      break;
case "signatureMessageConverter":

                      ....
                      break;
default:
throw new InvalidOperationException("Unrecognized element");

 

              }
      }
      reader.ReadEndElement();
   }
}

This this part mostly just uses the XmlReader to iterate over a policy defined in xml. The switch statement is where the action happens. Let's go through the cases in order

 

case "x509":
_x509Provider = new X509TokenProvider();
_x509Provider.ReadXml(reader, extensions);
break;

Here we're able to take advantage of X509TokenProvider and let it figure out to find and load the certificate.

 

case "signatureIssuer":
if (reader.IsEmptyElement)
throw new InvalidOperationException("Must specify a location for the signature issuer.");
reader.ReadStartElement();
_signatuerIssuer = new Uri(reader.Value);
reader.Read();
reader.ReadEndElement();
break;

The signatureIssuer specifies where the blind signature issuer is. We simply store the value for later when the policy assertion creates a client output filter.

 

case "signatureProtection":
if (reader.IsEmptyElement)
{
reader.ReadStartElement();
break;
}

 

        if (reader.NodeType == XmlNodeType.EndElement)
{
reader.ReadEndElement();
break;
}

 

        XmlReader protectionReader = reader.ReadSubtree();
protectionReader.ReadStartElement();
protectionReader.MoveToContent();

 

        if (protectionReader.NodeType != XmlNodeType.EndElement)
_signatureProtection = new Policy(GetSignaturePolicyAssertions(protectionReader, extensions));

 

        protectionReader.Close();
reader.MoveToContent();
reader.ReadEndElement();

 

        break;

This is where we create the policy that defines how the blind signature client communicates with the signature issuer. This code is mainly concerned with making sure the XmlReader is advanced properly. The code that creates the policy is in GetSignaturePolicyAssertions:

PolicyAssertion[] GetSignaturePolicyAssertions(XmlReader reader, IDictionary<string, Type> extensions)
{
List<PolicyAssertion> assertions = new List<PolicyAssertion>();
do
{
reader.MoveToContent();
Type assertionType = extensions[reader.Name];
if (assertionType == null)
throw new InvalidOperationException("No extension present for: " + reader.Name);

 

      PolicyAssertion assertion = Activator.CreateInstance(assertionType) as PolicyAssertion;
if (assertion == null)
throw new InvalidOperationException("Unable to create policy assertion for: " + reader.Name);
assertion.ReadXml(reader, extensions);
assertions.Add(assertion);
} while (reader.Read() && reader.NodeType != XmlNodeType.EndElement);
return assertions.ToArray();
}

 

This method reads over the children of the signatureProtection element. Note the assertions here must be present in the extensions. It would also be possible to hardcode common assertions to not have them listed in the extensions. Again we're using the assertion's own ReadXml method to initialize it. These assertions can be any of the builtin assertions or custom assertions.

 

case "signatureMessageConverter":
bool isConvertElementEmpty = reader.IsEmptyElement;
string converterType = reader.GetAttribute("type");

_serviceConverter = Activator.CreateInstance(Type.GetType(converterType)) as IMessageConverterServer;
if (_serviceConverter == null)
throw new InvalidOperationException("Unable to construct service converter.");
reader.ReadStartElement();
if (!isConvertElementEmpty) reader.ReadEndElement();
break;

This is one of the interfaces necessary for the BlindSignatureAssertion to communicate with the application. It has a single method:

byte[] GetMessageBytes(SoapEnvelope env);

Now the BlindSignatureAssertion is initialized, how does it construct the necessary filters? Since it only supports clients sending blind signatures to services, CreateClientInputFilter and CreateServiceOutputFilter both simply return null. CreateClientOutputFilter and CreateServiceInputFilter first verify the correct information has been provided and then create the filters:

public override SoapFilter CreateClientOutputFilter(FilterCreationContext context)
{
if (_signatuerIssuer == null) throw new InvalidOperationException("Must specify signaturer issuer for client.");
if (_x509Provider == null || _x509Provider.GetToken() == null)
throw new InvalidOperationException("Must specifying the signing certificate.");

return new BlindSignatureClientOutputFilter(_signatuerIssuer, _x509Provider.GetToken().Certificate, _signatureProtection);
}

public override SoapFilter CreateServiceInputFilter(FilterCreationContext context)
{
if (_x509Provider == null || _x509Provider.GetToken() == null)
throw new InvalidOperationException("Must specifying the signing certificate.");

return new BlindSignatureServiceInputFilter(_x509Provider.GetToken().Certificate, _serviceConverter);
}

BlindSignatureClientOutputFilter

When ProcessMessage is called on this class, it needs:

  • the URI of the signature issuer
  • the policy of the signature issuer
  • the certificate to generate blinding factors with
  • any client credentials that need to be presented to the signature issuer
  • an instance of an application defined class that can generate the necessary documents in byte[] form and select the correct document that was signed

The URI, certificate and policy are configured in the policy file. The application provides the client credentials and helper class through the OperationState property bag on the envelope's SoapContext. The helper class needs to implement this interface:

public interface IMessageConverterClient
{
BlindedMessageSet[] GetMessageSets();
int SetMessage(BlindedMessageSet messages,SoapEnvelope envelope);
}

In our system GetMessageSets generates a set of all the possible ballots, and creates an array of these ballot sets. Then it sets a different ballot id for each of these sets. Then it converts all of the ballots into byte[] and returns it. Essentially BlindedMessageSet[] is a byte[][][]. SetMessage takes the set of ballots that was signed and converts the byte[] back into ballots. Then it finds the ballot that was actually voted and modifies the body of the envelope to include the proper ballot id. It then returns the index of the ballot it found so the correct signature can be added to message headers.

The client output filter processes messages like this:

  1. verify all necessary data has been provided
  2. create a blind signature proxy
  3. set the client credentials and policy on the proxy
  4. get the unblinded documents from the application
  5. with the certificate, randomly generate a blinding factor for each set and then blind all the messages
  6. go through the blind signature protocol described in part 2
  7. unblind the documents in the signed message set
  8. pass the unblinded documents to the application to get the signature index
  9. create the blind signature header and insert it into the envelope

BlindSignatureServiceInputFilter

The service filter is simpler than the client filter, all it has to do is verify the signature. To do this it needs:

  • the certificate used to issue the signature
  • an instance of an application defined class that can convert the body of the envelope into a byte[]

Both of these are configured in policy. When ProcessMessage is called:

  1. it finds the header that contains the blind signature
  2. it converts the body of the envelope into a byte[]
  3. using the certificate it signs the byte[] and compares it to the blind signature
  4. if they match processing continues with any remaining assertions
  5. if they don't match processing terminates and a fault is returned to the client

In the next part, I'll trace a message all the way through the system, go into a little more detail on the blind signature issuer and wrap things up.

The other parts:

Part 1

Part 2

Part 3

Part 4

Part 5