Firmar un documento Xml con SignedXml

Creado el día 29/01/2010 13:18 por zorry

Vamos a aprender a firmar un documento Xml con el estándar W3C Xml-DSig. En el mundo .Net, la clase SignedXml nos implementa gran parte del estándar, con lo que con unas cuantas líneas podremos implementar una tarea tediosa tiempo atrás.

En el estándar se especifican tres tipos de firmas:

  • Enveloped: La firma se incluye dentro del Xml que se ha firmado
  • Enveloping: El Xml firmado se incluye dentro del Xml de la firma.
  • Detached: La firma se incluye en un archivo independiente del xml firmado.

Vamos a trabajar con firmas enveloped. Vamos a firmar un documento Xml, empleando un certificado digital X509 almacenado en el almacén local de la máquina, aprovechando el código que describí en este artículo. Supongamos que necesitamos firmar un Xml encargado de devolver un acuse de recibo a un servicio externo:

<?xml version="1.0" encoding="utf-8" standalone="no"?>
<corpme-floti>
    <acuses id="response">
		<credenciales>
			<entidad>pruebaxml1</entidad>
			<grupo>Grupo de prueba</grupo>
			<usuario>USUARIO_TEST</usuario>
			<email>usuario@grupo.com</email>
		</credenciales>
		<acuse>
			<referencia>ref001</referencia>
			<identificador>idok001</identificador>
		</acuse>
	</acuses>
</corpme-floti>

Pasamos a implementar nuestro método en C#, empleando como dijimos el objeto SignedXml.

public void SignXmlDocument(XmlDocument document, string    
  subjectName, string nodeToSign)
{
    //1. Load the Xml document
    SignedXml signer = new SignedXml(document);

    //2. Setup the key to sign
    X509Certificate2 cert = getCertificateFromStore(subjectName);
    signer.SigningKey = cert.PrivateKey;
            
    //3. Add public key info to allow checking the key
    signer.KeyInfo = new KeyInfo();
    KeyInfoX509Data keyInfoClause = new KeyInfoX509Data(cert);
    keyInfoClause.AddSubjectName(cert.SubjectName.Name);
    signer.KeyInfo.AddClause(keyInfoClause);

    //4. Set the reference to sign
    Reference reference = new Reference();
    reference.Uri = nodeToSign;
    reference.AddTransform(new XmlDsigEnvelopedSignatureTransform(false));
    signer.AddReference(reference);

    //5. Compute the signature
    signer.ComputeSignature();

    //6. Append node to the document
    XmlNode signatureNode = document.ImportNode(signer.GetXml(), true);
    document.DocumentElement.AppendChild(signatureNode);
}

Al cual llamaremos con este código:

XmlDocument doc = new XmlDocument();
//TODO: Load the Xml from somewhere

SignHelper signer = new SignHelper();
signer.SignXmlDocument(doc, "CN=TestCertificate", "#response");

Expliquemos que es lo que realiza este método:

  1. Cargamos el documento Xml dentro de la clase SignedXml.
  2. Obtenemos el certificado digital del almacén de certificados (obtenemos el certificado con el Canonical Name contenido en el parámetro subjectName), y como vamos a realizar una firma, introducimos la clave privada del certificado en .SigningKey
  3. Para dar la posibilidad al receptor de comprobar la firma sin necesidad de tener el certificado, introducimos la clave pública del certificado dentro de la propiedad .KeyInfo. Como referencia y de manera opcional, introducimos el Canonical Name del certificado.
  4. Como no deseamos firmar el documento Xml entero, sino el nodo acuses, introducimos el valor de su atributo id (viene contenido en el parámetro nodeToSign con valor “#response”) como referencia. En nuestro caso, ya que vamos ah hacer una firma Enveloped, es necesario firmar un nodo en lugar del Xml completo porque sino, no se podría luego validar la firma. (Para firmar el documento completo, reference.Uri ha de ser una cadena vacía).
  5. Calculamos la firma.
  6. Introducimos el Xml con la firma dentro del documento Xml.

Esta llamada nos generará el siguiente Xml (he acortado las partes no relevantes por razones de espacio):

<?xml version="1.0" encoding="utf-8" standalone="no"?>
<corpme-floti>
  <acuses id="response">
    [...]
  </acuses>
  <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
    <SignedInfo>
      <CanonicalizationMethod
        Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
      <SignatureMethod 
           Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
      <Reference URI="#response">
        <Transforms>
          <Transform Algorithm=
              "http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
        </Transforms>
        <DigestMethod 
            Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
        <DigestValue>b5tiyzt9vu8VQvT/ki3nMefnH1Y=</DigestValue>
      </Reference>
    </SignedInfo>
    <SignatureValue>YMmcs+g7ciHQg9s48Oh[…]1Jwvt+vVnKc=</SignatureValue>
    <KeyInfo>
      <X509Data>
        <X509SubjectName>CN=TestCertificate</X509SubjectName>
        <X509Certificate>MIIBtzCCAWWg[…]OKueoqv4jgTAY=</X509Certificate>
      </X509Data>
    </KeyInfo>
  </Signature>
</corpme-floti>

Finalmente, podríamos comprobar la firma generada con la siguiente función, también empleando SignedXml:

public bool ValidateXmlSignature(XmlDocument document)
{
    XmlNamespaceManager nsMgr = new XmlNamespaceManager(document.NameTable);
    nsMgr.AddNamespace("ds", "http://www.w3.org/2000/09/xmldsig#");
            
    SignedXml verifier = new SignedXml(document);
    XmlElement signatureElement = 
      document.DocumentElement.SelectSingleNode("ds:Signature", nsMgr) 
      as XmlElement;
    verifier.LoadXml(signatureElement);
    return verifier.CheckSignature();
}

Comentarios no permitidos