Colabora .NET

Encriptar mensajes SOAP

Manejar extensiones SOAP de .NET

 

Fecha: 31/Oct/2006 (24/10/2006)
Autor: David Elvar - davidelcam@hotmail.com

 


Introducción

En este artículo se trata la forma de acceder al mensaje Soap enviado y recibido por un servicio web desarrollado en ASP .NET y como podemos encriptar dicho mensaje, total o parcialmente, para crear una comunicación segura.

Ciclo de vida de un mensaje Soap.

Cuando se hace una llamada a un servicio web .NET genera un  objeto SoapMessage que pasa por diferentes estados a lo largo de la comunicación entre el cliente que consume el servicio y el servidor donde está publicado dicho servicio. Cada uno de estos estados está definido por la propiedad Stage del mensaje:

  1. El cliente envía una solicitud al servicio web. El SoapMessageStage se establece en BeforeSerialize.
     
  2. ASP .NET serializa el código generando un mensaje Soap que será trasmitido al servidor. El SoapMessageStage se establece en AfterSerialize.
     
  3. El mensaje SOAP llega al servidor donde se ubica el servicio web. El SoapMessageStage se establece en BeforeDeserialize.
     
  4. El mensaje Soap se deserializa para que sea tratado. El SoapMessageStage se establece en AfterDeserialize.
     
  5. El servidor genera una respuesta a la solicitud. El SoapMessageStage se establece en BeforeSerialize.
     
  6. ASP .NET serializa el código generando un mensaje Soap que será trasmitido al cliente como respuesta a su solicitud. El SoapMessageStage se establece en AfterSerialize.
     
  7. La respuesta llega al cliente. El SoapMessageStage se establece en BeforeDeserialize.
     
  8. El mensaje SOAP se deserializa para que pueda ser tratado por el cliente. El SoapMessageStage se establece en AfterDeserialize.
     

Utilizando extensiones Soap de .NET.

Para poder acceder al buffer de memoria donde .NET almacena el mensaje Soap enviado y recibido por un servicio web se encuentran a nuestra disposición dentro del Framework de .NET las denominadas extensiones Soap. Para crear nuestra propia extensión Soap simplemente deberemos crear una clase que derive de SoapExtension sobeescribiendo una serie de métodos abstractos.

Relacionada directamente con estas extensiones existe la clase SoapExtensionAttribute . Para poder utilizarla deberemos crear igualmente una clase derivada. Esta extensión de atributo va a ser la que diga a nuestro servicio web la extensión Soap asociada que debe ejecutar.

El Código.

Lo primero que hacemos es definir un par de enumerados que nos van a permitir personalizar si queremos encriptar la solicitud del cliente o la respuesta del servidor y que parte del mensaje Soap generado tras la serialización queremos procesar en cada caso.

// Lista de los tipos mensajes que intervienen en la comunicación.
public enum EncryptMode
{
	None = 0,
	Request = 1,
	Response = 2,
	Message = 3
}
// Lista de las distintas partes del mensaje SOAP que podemos enciptar
public enum Target
{
	Header = 1,
	Body = 2,
	Envelope = 3
}

Para generar nuesta propia extensión de atributo Soap creamos una clase derivada de SoapExtensionAttribute.

[AttributeUsage(AttributeTargets.Method)]
public class  SoapEncryptionExtensionAttribute : SoapExtensionAttribute
{
}

Nota:
A través de AttributeUsage estamos diciendo a que parte del código se puede aplicar nuestro atributo. En nuestro caso se podrá aplicar a un método del servicio web.

Dentro de esta clase debemos sobreescribir obligatoriamente dos propiedades abstractas: Priority y TypeExtension. La primera indica la prioridad en la que debe ejecutarse la extensión Soap asociada al atributo con respecto a otras extensiones configuradas para el mismo método de nuestro servicio web. La segunda y mas importante es una propiedad de solo lectura y es la que le indica al servicio web que extensión Soap debe ejecutar.


/// <summary>
/// Prioridad de la extensión SOAP.
/// </summary>
private int m_priority = 0;
public override int Priority
{
	get { return m_priority; }
	set { m_priority = value; }
}

/// <summary>
 /// Declaración del tipo de la extensión SOAP que queremos
/// que se ejecute junto con nuestro servicio web.
/// </summary>
public override Type ExtensionType
{
	get { return typeof(SoapEncryptionExtension); }
}

/// <summary>
/// Parte de la comunicación que se desea a encriptar.
/// </summary>
private EncryptMode m_encrypt;
public EncryptMode Encrypt
{
	get { return m_encrypt; }
	set { m_encrypt = value; }
}

Es muy importante tener en cuenta que siempre que generemos una clase derivada de SoapExtensionAttribute deberan aparecer las dos propiedades descritas anteriormente. En nuestro caso particular también deberemos definir dos propiedades mas, una por cada enumerado.

Una vez definida nuestra extensión de atributo debemos generar la propia extensión Soap que es la que va a llevar toda la lógica del proceso. Para ello creamos una clase derivada de SoapExtension.

public class SoapEncryptionExtension : SoapExtension
{
}

Siempre que generemos una clase derivada de  SoapExtension existen varios métodos abstractos que debemos sobreescribir.

/// <summary>
/// Este método se llama la primera vez que el servicio web accede a la extensión SOAP.
/// Almacena en Caché el objeto devuelto.
/// </summary>
/// <param name="methodInfo"></param>
/// <param name="attribute"></param>
public override object GetInitializer(LogicalMethodInfo methodInfo, 
			SoapExtensionAttribute attribute)
{
	return (SoapEncryptionExtensionAttribute)attribute;
}

/// <summary>
/// Este método se llama la primera vaz que el servicio web accede a la extensión SOAP.
/// </summary>
/// <param name="initializer">
/// Objeto almacenado en Caché tras la llamada a la función GetInitializer
/// </param>
public override void Initialize(object initializer)
{

	SoapEncryptionExtensionAttribute attribute = 
		(SoapEncryptionExtensionAttribute)initializer;
	m_logSOAPMessage = attribute.LogSOAPMessage;
	m_logFileName = attribute.LogFileName;
	m_encrypt = attribute.Encrypt;
	m_SOAPTarget = attribute.SOAPTarget;
}

/// <summary>
/// Permite acceder al buffer de memoria que contiene la solicitud o el mensaje SOAP.
/// </summary>
/// <param name="stream">Stream que contiene el mensaje SOAP serializado
/// en el campo BeforeDeserialize de SoapMessage.
/// <returns>Stream que contiene el mensaje SOAP serializado
/// en el campo AfterSerialize de SoapMessage.</param>
/// <returns></returns>
public override System.IO.Stream ChainStream(System.IO.Stream stream)
{
	m_oldStream = stream;
	m_newStream = new MemoryStream();
	return m_newStream;
}

/// <summary>
/// Procesa el mensaje SOAP en cada una de sus distintas fases.
/// </summary>
/// <param name="message">Mensaje SOAP.</param>
public override void ProcessMessage(SoapMessage message)
{
	try
	{
		switch(message.Stage)
		{
			case SoapMessageStage.BeforeSerialize:
				break;
			case SoapMessageStage.AfterSerialize:
				EncryptSoap(message);
				break;
			case SoapMessageStage.BeforeDeserialize:
				DecryptSoap(message);
				break;
			case SoapMessageStage.AfterDeserialize:
				break;
			default:
				throw new Exception("Estado de mensaje SOAP no válido.");
		}
	}
	catch(Exception ex)
	{
		System.Diagnostics.Trace.WriteLine(ex.Message);
	}
}

Cuando se realiza una llamada al método del servicio web configurado  con nuestra extensión Soap .NET llama a la función GetInitializer de la clase que devuelve un objeto de tipo SoapEncryptionExtensionAttribute cuyo estado se almacena en Caché. Inmediatamente despues .NET llama al método Initializer pasando como parámetro el objeto almacenado en Caché, el cual nos sirve para inicializar las variables necesarias en el proceso.

El método ProcessMessage se utiliza para poder detectar los distintos estados por los que va a pasar el mensaje Soap dentro del proceso de comunicación. Para saber el estado actual del mensaje accedemos a la propiedad Stage del objeto SoapMessage recibido como parámetro. Dependiendo si el servicio web se ha configurado del lado del cliente o del lado del servidor el parámetro recibido podrá almacenarse en un objeto del tipo SoapClientMessage o SoapServerMessage respectivamente. Tanto para mensajes del lado del cliente como del servidor haremos la llamada al método que encripta cuando el mensaje se haya serializado (Afterserialize) y lo desencriptaremos justo antes de que vuelva a serializarse (BeforeDeserialize).

Para poder modificar el mensaje Soap inicializamos un objeto XmlDocument con un stream que va a contener, según corresponda, la solicitud o la respuesta Soap. Dependiendo de que parte del mensaje queramos encriptar accderemos a un nodo en concreto utilizando una consulta xpath que pasamos como parámetro a la función SelecySingleNode definida para el objeto XmlDocument. El texto al que le vamos a aplicar el algoritmo será el valor devuelto por la propiedad InnerXml del objeto XmlNode que retorna esta función.

public  CryptoSoap(CryptoSoapDelegate cryptoSoapDelegate, string xpath, Stream stream)
{
	try
	{
		// inicializo vsariables
		System.Xml.XmlDocument xdoc = new System.Xml.XmlDocument();;
		System.Xml.XmlNamespaceManager nsmgr = null;
		System.Xml.XmlNode xnode = null;

		// cargo el mensaje soap en un xml
		stream.Position = 0;
		xdoc.Load(stream);

		// agrego los namespaces
		nsmgr = new System.Xml.XmlNamespaceManager(xdoc.NameTable);
		nsmgr.AddNamespace("soap", @"http://schemas.xmlsoap.org/soap/envelope/");

		// accedo al nodo del mensaje soap que voy a tratar
		xnode = xdoc.SelectSingleNode(xpath, nsmgr);
		if(xnode != null)
			xnode.InnerXml = cryptoSoapDelegate(xnode.InnerXml);

		m_newStream.Position = 0;
		xdoc.Save(m_newStream);
		m_newStream.Position = 0;
	}
	catch(Exception ex)
	{
		encryptedText = ex.Message;
	}
}
								

Para realizar la encriptación y desencriptación del mensaje utilizamos un  algoritmo simétrico o de clave privada. Esto es debido a que es posible que podamos trabajar con cantidades grandes de datos y los algoritmos asimétricos o de clave pública son mas recomendables cuando se trabaja con pequeñas cantidades. El principal inconveniente que tienen este tipo de algoritmos de clave pública es que las dos partes que intervienen en el proceso de comunicación deben acordar previamente unos valores de clave privada y de vector de inicialización. En el ejemplo que estamos viendo se almacena la clava privada y el VI en dos variables para simplificar el proceso. Una práctica que se recomienda en estos casos es trabajar con una mezcla de algoritmo simétrico y asimétrico. En primera instancia el emisor del mensaje genera una clave pública utilizando el cifrado asimétrico que es enviada al receptor. Seguidamente este último genera una clave privada y un VI con un cifrado simétrico, los cuales son enviados al emisor en un mensaje cifrado mediante la clave pública recibida en primera instancia. De esta forma las dos partes que participan en la comunicación serán conoceras de la clave privada y el VI necesarios para trabajar con algoritmo simétrico y esta clave será diferente en cada comunicación.

public string EncryptTextNode(string text)
{
	string encryptedText = string.Empty;
	try
	{
		// creo un proveedor criptográfico para el algoritmo simétrico TripleDES
		TripleDESCryptoServiceProvider tDESalg = 
					new TripleDESCryptoServiceProvider();
		// creo un stream para escibir el texto encriptado
		MemoryStream mstream = new MemoryStream();
		CryptoStream cstream = new CryptoStream(
			mstream, tDESalg.CreateEncryptor(m_key, m_IV), 
			CryptoStreamMode.Write);
		StreamWriter swriter = new StreamWriter(cstream);

		// escribo el texto a encriptar
		swriter.Write(text);

		// cierro todos los streams
		swriter.Flush();
		swriter.Close();
		cstream.Close();
		mstream.Close();

		encryptedText = Convert.ToBase64String(mstream.ToArray());
	}
	catch(Exception ex)
	{
		encryptedText = ex.Message;
	}
	return encryptedText;
}

public string DecryptTextNode(string text)
{
	string decryptedText = string.Empty;
	try
	{
		// creo un proveedor criptográfico para el algoritmo simétrico TripleDES
		TripleDESCryptoServiceProvider tDESalg = 
			new TripleDESCryptoServiceProvider();
		// creo un stream para escibir el texto desencriptado
		MemoryStream mstream = new MemoryStream(Convert.FromBase64String(text));
		CryptoStream cstream = new CryptoStream(
			mstream, tDESalg.CreateDecryptor(m_key, m_IV), 
			CryptoStreamMode.Read);
		StreamReader sreader = new StreamReader(cstream);

		// escribo el texto a desencriptar
		decryptedText = sreader.ReadToEnd();

		// cierro todos los streams
		sreader.Close();
		cstream.Close();
		mstream.Close();
	}
	catch(Exception ex)
	{
		decryptedText = ex.Message;
	}
	return decryptedText;
}

 

Nota:
Para hacer que nuestro servicio web funcione correctamente junto con la extensión Soap es importante que configuremos el método correspondiente indicando los atributos que sean  necesarios tanto del  lado del servidor, en el propio código del servicio, como del lado del cliente, utilizando la clase proxy que te genera .NET al agregar una referencia web.


Resumen.

En el ejemplo que hemos visto como podemos acceder a la zona de memoria donde .NET almacena el mensaje Soap enviado y recibido por un servicio web y hemos aplicado un algoritmo de encriptación para proteger la comunicación. Mas allá de este caso particular es importante que nos quedemos con el concepto de las extensiones Soap y que tengamos claro como funcionan ya que nos van a permitir en cualquier momento ampliar la funcionalidad de nuestros servicios web de una forma relativamente sencilla.

En todo caso espero que el artículo haya sido lo suficientemente claro y en un futuro pueda aportar mas colaboraciones. Para cualquier duda o sugerencia podéis poneros en contacto con migo a través de mi dirección de correo davidelcam@hotmail.com.


Espacios de nombres usados en el código de este artículo:

using System;
using System.IO;
using System.Xml;
using System.Security.Cryptography;
using System.Web.Services.Protocols;

Código de ejemplo (ZIP):

 

Fichero con el código de ejemplo: davidel_EncriptarMensajesSoap.zip - 30.4 KB

(MD5 checksum: 17747B3D092A1FB8FC38449BF2502E12

 


ir al índice principal del Guille