MessageSecurity in PeerChannel

(updated to reflect Feb CTP changes)

One of the cool things in PeerChannel is the ability to secure application messages. say, you are writing an app that uses peerchannel to download content or broadcast stock quotes, you want to be sure that the message originated from a known source. This is because, in a peer mesh, you are not directly connected to the source, but thru other peer application instances. so a secure connection by itself doesnt guarantee application message security. What you also want to do is, to make sure that the message is not tampered in-flight while the message is traveling across the mesh. For this purpose, PeerChannel provides a property to enable message authentication. This property is available conviniently on NetPeerTcpBinding.

How it works?

for OutputChannels, each message is signed using the certificate provided thru PeerCredential.Certificate. All messages, before delivered to the application, are checked to tally the message credential using the validator(an instance of X509CertificateValidator) provided in PeerCredential.MessageSenderAuthentication.

lets consider an example. assume the following service contract for the mesh:

[

ServiceContract(Namespace = "https://Microsoft.ServiceModel.Samples.PeerChannel")]

[

PeerBehavior]

public interface IQuoteChange

{

[

OperationContract(IsOneWay = true)]

void PriceChange(string item, double change, double price);

}

public interface IQuoteChannel : IQuoteChange, IClientChannel

{

}

Sender

sender's code looks like this:

using (ChannelFactory<IQuoteChannel> cf = new ChannelFactory<IQuoteChannel>("BroadcastEndpoint"))

{

//load the certificate of the sender.

   X509Certificate2 senderCredentials = GetCertificate(StoreName.My, StoreLocation.CurrentUser, recognizedSender, X509FindType.FindBySubjectDistinguishedName);

cf.Credentials.Peer.Certificate = senderCredentials;

//specify a validator that is invoked during message signature verification.

cf.Credentials.Peer.MessageSenderAuthentication.CertificateValidationMode = X509CertificateValidationMode.Custom;

cf.Credentials.Peer.MessageSenderAuthentication.CustomCertificateValidator =

new SenderValidator(senderCredentials);

   using (IQuoteChannel sender = (IQuoteChannel)cf.CreateChannel())

{

      // Open the sender

sender.Open();

      string name = "InfoSolo";

      double change = Double.Parse("45");

      double currentValue = Double.Parse("100");

// The price change message is signed with sender credentials before sending on the mesh.

      sender.PriceChange(name, change, currentValue);

   }

}

 

Receiver

on the receiver side, peerchannel passes the message thru message signature verification before forwarding it to the application. the OperationContext holds security properties that resulted after the message verification. Here is the receiver of price change messages: This sample simply finds a claim in the operation context object and prints it:

Claim

FindClaim(ServiceSecurityContext context)

{

Claim result = null;

foreach (ClaimSet set in context.AuthorizationContext.ClaimSets)

{

IEnumerator<Claim> claims = set.FindClaims(ClaimTypes.Name, null).GetEnumerator();

if (claims.MoveNext())

{

result = claims.Current;

break;

}

}

return result;

}

public void PriceChange(string item, double change, double price)

{

ServiceSecurityContext context = ServiceSecurityContext.Current;

Claim claim = FindClaim(context);

string source = "unknown";

if (claim != null)

{

source = claim.Resource

as string;

}

Console.WriteLine("{0}=>(item: {1}, change: {2}, price: {3})", source,item, change.ToString("C"), price.ToString("C"));

}

 

MessageValidator

lastly, how to implement a message validator:

the validator that you pass to the method PeerSecurityBehavior.SetMessageX509Authentication() must implement a single method that identifies the known message source.

A sample implementation looks like this:

class

PublisherValidator : X509CertificateValidator

{

string senderThumbprint;

public PublisherValidator(X509Certificate2 sender)

{

if (sender == null)

throw new ArgumentException("sender");

this.senderThumbprint = sender.Thumbprint;

}

//single method that needs to be overriden. the single parameter certificate is extracted from the incoming message and passed to this function.

//all you need to do is to check if the certificate is valid and is one of the authenticated message senders that your application accepts.

public override void Validate(X509Certificate2 certificate)

{

if (0 != String.CompareOrdinal(certificate.Thumbprint, senderThumbprint))

throw new SecurityTokenValidationException("Unrecognized sender");

}

}

 

here is the complete exampe:

sender.cs

// Copyright (c) Microsoft Corporation. All Rights Reserved.

using

System;

using

System.Configuration;

using

System.ServiceModel;

using

System.ServiceModel.Channels;

using

System.ServiceModel.Security.Tokens;

using

System.Security.Cryptography.X509Certificates;

// M:N broadcast application that enables senders to send announcements to multiple receivers

// using Peer Channel (a multi-party channel). The receiver is implemented by a different program.

// If you are unfamiliar with new concepts used in this sample, refer to the Indigo Basic\GettingStarted sample.

namespace

Microsoft.ServiceModel.Samples

{

// Service contract for Broadcast. It's manually generated since Peer Transport currently does not

// support WSDL generation.

// Applying [PeerBehavior] attribute on the service contract enables retrieval of PeerNode from IClientChannel.

[

ServiceContract(Namespace = "https://Microsoft.ServiceModel.Samples.PeerChannel")]

[

PeerBehavior]

public interface IQuoteChange

{

[

OperationContract(IsOneWay = true)]

void PriceChange(string item, double change, double price);

}

public interface IQuoteChannel : IQuoteChange, IClientChannel

{

}

class SenderValidator : X509CertificateValidator

{

string senderThumbprint;

public SenderValidator(X509Certificate2 sender)

{

if (sender == null)

throw new ArgumentException("sender");

this.senderThumbprint = sender.Thumbprint;

}

public override void Validate(X509Certificate2 certificate)

{

if (0 != String.CompareOrdinal(certificate.Thumbprint, senderThumbprint))

throw new SecurityTokenValidationException("Unrecognized sender");

}

}

// Sender implementation code.

// Host the sender within this EXE console application.

public static class BroadcastSender

{

public static void Main()

{

// Get the sender ID from configuration

string senderId = ConfigurationManager.AppSettings["sender"];

string recognizedSender = "CN="+senderId;

// Create the sender with the given endpoint configuration

// Sender is an instance of the sender side of the broadcast application that has opened a channel to mesh

using (ChannelFactory<IQuoteChannel> cf = new ChannelFactory<IQuoteChannel>("BroadcastEndpoint"))

{

X509Certificate2 senderCredentials = GetCertificate(StoreName.My, StoreLocation.CurrentUser, recognizedSender, X509FindType.FindBySubjectDistinguishedName);

cf.Credentials.Peer.Certificate = senderCredentials;

cf.Credentials.Peer.MessageSenderAuthentication.CertificateValidationMode = X509CertificateValidationMode.Custom;

cf.Credentials.Peer.MessageSenderAuthentication.CustomCertificateValidator =

new SenderValidator(senderCredentials);

using (IQuoteChannel sender = (IQuoteChannel)cf.CreateChannel())

{

// Retrieve the PeerNode associated with the sender and register for online/offline events

// PeerNode represents a node in the mesh. Mesh is the named collection of connected nodes.

PeerNode node = sender.GetProperty.Find<PeerNode>();

node.Online +=

new EventHandler(OnOnline);

node.Offline +=

new EventHandler(OnOffline);

Console.WriteLine("** {0} is ready", senderId);

// Open the sender

sender.Open();

//Info that sender sends out to the receivers (mesh))

string name = "InfoSolo";

double change = Double.Parse("45");

double currentValue = Double.Parse("100");

Console.WriteLine("Press <ENTER> to broadcast message to receivers.");

Console.ReadLine();

Console.WriteLine("Sending Source, Price Change, and Current Value to receivers");

sender.PriceChange(name, change, currentValue);

Console.WriteLine("Press <ENTER> to terminate sender");

Console.ReadLine();

}

}

}

// PeerNode event handlers

static void OnOnline(object sender, EventArgs e)

{

Console.WriteLine("** Online");

}

static void OnOffline(object sender, EventArgs e)

{

Console.WriteLine("** Offline");

}

static internal X509Certificate2 GetCertificate(StoreName storeName, StoreLocation storeLocation, string key, X509FindType findType)

{

X509Certificate2 result;

X509Store store = new X509Store(storeName, storeLocation);

store.Open(

OpenFlags.ReadOnly);

try

{

X509Certificate2Collection matches;

matches = store.Certificates.Find(findType, key,

false);

if (matches.Count > 1)

throw new InvalidOperationException(String.Format("More than one certificate with key '{0}' found in the store.", key));

if (matches.Count == 0)

throw new InvalidOperationException(String.Format("No certificates with key '{0}' found in the store.", key));

result = matches[0];

}

finally

{

store.Close();

}

return result;

}

}

}

Sender.config

<?

xml version="1.0" encoding="utf-8" ?>

<

configuration>

<

appSettings>

<!--

use appSetting to configure the senderId -->

<

add key="sender" value="sender" />

</

appSettings>

<

system.serviceModel>

<

client>

<!--

Broadcast sender -->

<

endpoint name="BroadcastEndpoint"

address="net.p2p://FreeQuotes/Stocks"

binding="netPeerTcpBinding"

bindingConfiguration="BroadcastBinding"

contract="Microsoft.ServiceModel.Samples.IQuoteChange">

</

endpoint>

</

client>

<

bindings>

<

netPeerTcpBinding>

<

binding name="BroadcastBinding" port="31081" peerNodeAuthenticationMode="None" messageAuthentication="true" resolverType="" />

</

netPeerTcpBinding>

</

bindings>

</

system.serviceModel>

</

configuration>

 

Receiver.cs

// Copyright (c) Microsoft Corporation. All Rights Reserved.

using

System;

using

System.Collections;

using

System.Collections.Generic;

using

System.Configuration;

using

System.Security;

using

System.Security.Authorization;

using

System.Security.Cryptography.X509Certificates;

using

System.ServiceModel;

using

System.ServiceModel.Channels;

using

System.ServiceModel.Security;

using

System.ServiceModel.Security.Tokens;

// M:N broadcast application that enables senders to send announcements to multiple receivers

// using Peer Channel (a multi-party channel). The sender is implemented by a different program.

// If you are unfamiliar with new concepts used in this sample, refer to the Indigo Basic\GettingStarted sample.

namespace

Microsoft.ServiceModel.Samples

{

// Define a service contract.

[

ServiceContract(Namespace="https://Microsoft.ServiceModel.Samples")]

public interface IBroadcast

{

[

OperationContract(IsOneWay = true)]

void Announce(string msg);

}

[

ServiceContract(Namespace="https://Microsoft.ServiceModel.Samples.PeerChannel")]

[

PeerBehavior]

public interface IQuoteChange

{

[

OperationContract(IsOneWay = true)]

void PriceChange(string item, double change, double price);

}

class PublisherValidator : X509CertificateValidator

{

string senderThumbprint;

public PublisherValidator(X509Certificate2 sender)

{

if (sender == null)

throw new ArgumentException("sender");

this.senderThumbprint = sender.Thumbprint;

}

public override void Validate(X509Certificate2 certificate)

{

if (0 != String.CompareOrdinal(certificate.Thumbprint, senderThumbprint))

throw new SecurityTokenValidationException("Unrecognized sender");

}

}

[

ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]

public class BroadcastReceiver : IQuoteChange

{

// PeerNode event handlers

static void OnOnline(object sender, EventArgs e)

{

Console.WriteLine("** Online");

}

static void OnOffline(object sender, EventArgs e)

{

Console.WriteLine("** Offline");

}

Claim FindClaim(ServiceSecurityContext context)

{

Claim result = null;

foreach (ClaimSet set in context.AuthorizationContext.ClaimSets)

{

IEnumerator<Claim> claims = set.FindClaims(ClaimTypes.Name, null).GetEnumerator();

if (claims.MoveNext())

{

result = claims.Current;

break;

}

}

return result;

}

public void PriceChange(string item, double change, double price)

{

ServiceSecurityContext context = ServiceSecurityContext.Current;

Claim claim = FindClaim(context);

string source = "unknown";

if (claim != null)

{

source = claim.Resource

as string;

}

Console.WriteLine("{0}=>(item: {1}, change: {2}, price: {3})", source,item, change.ToString("C"), price.ToString("C"));

}

// Host the receiver within this EXE console application.

public static void Main()

{

// Get base address from app settings in configuration

Uri baseAddress = new Uri(ConfigurationManager.AppSettings["baseAddress"]);

string recognizedPublisherName = ConfigurationManager.AppSettings["publisherQName"];

X509Certificate2 publisherCredentials = null;

ServiceHost receiver = new ServiceHost(new BroadcastReceiver(), new Uri[] { baseAddress });

publisherCredentials = GetCertificate(

StoreName.TrustedPeople, StoreLocation.CurrentUser, recognizedPublisherName, X509FindType.FindBySubjectDistinguishedName);

//this settings specifies that only messages signed with above cert should be accepted.

receiver.Credentials.Peer.MessageSenderAuthentication.CertificateValidationMode = X509CertificateValidationMode.Custom;
receiver.Credentials.Peer.MessageSenderAuthentication.CustomCertificateValidator = new PublisherValidator(publisherCredentials);

NetPeerTcpBinding binding = new NetPeerTcpBinding("Binding1");

// Retrieve the PeerNode associated with the receiver and register for online/offline events

// PeerNode represents a node in the mesh. Mesh is the named collection of connected nodes.

            EndpointAddress lookFor = new EndpointAddress(baseAddress + "Stocks");
for (int i=0; i<receiver.ChannelDispatchers.Count; ++i)
{
ChannelDispatcher channelDispatcher = receiver.ChannelDispatchers[i] as ChannelDispatcher;
if (channelDispatcher != null)
{
for (int j=0; j<channelDispatcher.Endpoints.Count; ++j)
{
EndpointDispatcher endpointDispatcher = channelDispatcher.Endpoints[j];
if (endpointDispatcher.EndpointAddress == lookFor)
{
IOnlineStatus ostat = (IOnlineStatus)channelDispatcher.Listener.GetProperty<IOnlineStatus>();
if (ostat != null)
{
ostat.Online += OnOnline;
ostat.Offline += OnOffline;
if (ostat.IsOnline)
{
Console.WriteLine("** Online");
}
}
}
}
}
}

// Open the ServiceHostBase to create listeners and start listening for messages.

receiver.Open();

// The receiver can now receive broadcast announcements

Console.WriteLine("** The receiver is ready");

Console.WriteLine("Press <ENTER> to terminate receiver.");

Console.ReadLine();

// Close the ServiceHostBase to shutdown the receiver

receiver.Close();

}

static internal X509Certificate2 GetCertificate(StoreName storeName, StoreLocation storeLocation, string key, X509FindType findType)

{

X509Certificate2 result;

X509Store store = new X509Store(storeName, storeLocation);

store.Open(

OpenFlags.ReadOnly);

try

{

X509Certificate2Collection matches;

matches = store.Certificates.Find(findType, key,

false);

if (matches.Count > 1)

throw new InvalidOperationException(String.Format("More than one certificate with key '{0}' found in the store.", key));

if (matches.Count == 0)

throw new InvalidOperationException(String.Format("No certificates with key '{0}' found in the store.", key));

result = matches[0];

}

finally

{

store.Close();

}

return result;

}

}

}

Receiver.config

<?

xml version="1.0" encoding="utf-8" ?>

<

configuration>

<

appSettings>

<!--

use appSetting to configure base address provided by host -->

<

add key="baseAddress" value="net.p2p://FreeQuotes" />

<

add key="publisherQName" value="CN=sender" />

<

add key="member" value="receiver1" />

</

appSettings>

<

system.serviceModel>

<

services>

<!--

Broadcast receiver -->

<

service

type="Microsoft.ServiceModel.Samples.BroadcastReceiver">

<!--

use base address provided by the host -->

<

endpoint address="Stocks"

binding="netPeerTcpBinding"

bindingConfiguration="Binding1"

contract="Microsoft.ServiceModel.Samples.IQuoteChange" />

</

service>

</

services>

<

bindings>

<

netPeerTcpBinding>

<

binding name="Binding1" port="31084" peerNodeAuthenticationMode="None" messageAuthentication="true" resolverType="" />

</

netPeerTcpBinding>

</

bindings>

</

system.serviceModel>

</

configuration>

Note: MessageAuthentication works independent of PeerNodeAuthenticationMode setting. For best security, turn both settings on.

-Ram Pamulapati

This posting is provided "AS IS" with no warranties, and confers no rights. Use of included script samples are subject to the terms specified at: https://www.microsoft.com/info/cpyright.htm"