How To: Datensignierung mit .NET

Nach dem ich über Verschlüsselung und Hashing geschrieben habe, kommen wir zum Signieren von Daten. Dies dient vor allem dazu zu überprüfen ob Daten verändert wurden und ist zum Beispiel beim E-Mailverkehr notwendig. Da dort auf dem Weg vom Sender zum Empfänger Daten leicht verändert werden oder sich jemand anderes für einen mir bekannten Adressaten ausgeben könnte.

Signierung mit DSA

Es gibt zwei Möglichkeiten Datenpakete zu kennzeichnen. Entweder wir nutzen tatsächlich die gegeben Daten oder wir erstellen aus ihnen einen Hash welcher dann signiert wird. Letzteres ist aus Performanzgründen in aller Regel vor allem bei größeren Datenmengen vorzuziehen.

Der Quellcode für die Signierung und Validierung der Daten ist denkbar einfach. Signierung einer Datei:

// Read data
FileStream stream = File.OpenRead(path);
BinaryReader reader = new BinaryReader(stream);
byte[] data = reader.ReadBytes((int) stream.Length);
stream.Close();

// create algorithm and sign
DSACryptoServiceProvider algorthim = new DSACryptoServiceProvider();
Signature = algorthim.SignData(data);
ExportParameters = algorthim.ExportParameters(true);

Validierung der Signierung:

// Read data
FileStream stream = File.OpenRead(path);
BinaryReader reader = new BinaryReader(stream);
byte[] data = reader.ReadBytes((int)stream.Length);
stream.Close();

// create algorithm and sign
DSACryptoServiceProvider algorthim = new DSACryptoServiceProvider();
algorthim.ImportParameters(ExportParameters);
bool isValid = algorthim.VerifyData(data, Signature);

Für die Validierung habe ich an dieser Stelle die exportierten Schlüsselinformationen in der Property ExportParameters gespeichert. Wie man diese bekommt und damit umgeht wurde in einem vorherigen Post über Verschlüsselung beschrieben. Die eigentliche Signatur ist ein byte[] und wird in meinem Beispiel in einer Property hinterlegt.

Signierung mit RSA

Die Signierung mit RSA folgt dem gleichen Muster wie der mit DSA. Um es etwas „aufregender“ zu gestalten wird diesmal ein Hash signiert:

// Read data
FileStream stream = File.OpenRead(path);
BinaryReader reader = new BinaryReader(stream);
byte[] data = reader.ReadBytes((int)stream.Length);
stream.Close();
// Compute hash
MD5CryptoServiceProvider hashAlgorithm = new MD5CryptoServiceProvider();
hashAlgorithm.ComputeHash(data);

// sign data
RSACryptoServiceProvider algorithm = new RSACryptoServiceProvider();
Signature = algorithm.SignHash(hashAlgorithm.Hash, CryptoConfig.MapNameToOID("MD5"));

Validierung:

// Read data
FileStream stream = File.OpenRead(path);
BinaryReader reader = new BinaryReader(stream);
byte[] data = reader.ReadBytes((int)stream.Length);
stream.Close();
RSACryptoServiceProvider algorithm = new RSACryptoServiceProvider();
algorithm.ImportParameters(ExportParameters);
bool isValid = algorithm.VerifyHash(Hash, CryptoConfig.MapNameToOID("MD5"), Signature);

Wie man sehen kann wird dem Algorithmus kein Objekt sondern der Name des Hashalgorithmus übergeben. Wobei Name falsch ist, genauer handelt es sich um eine ID welche man am besten über die CryptoConfig Klasse ermittelt. Denn intern wird anhand dieser ID ein Integer ermittelt der den Algorithmus innerhalb der CryptoAPI kennzeichnet. Welche Namen man MapNameToOID übergeben muss, findet sich in der MSDN.

Signierung von XML-Dateien

Abschließend möchte ich ein komplexeres Beispiel aus der MSDN erläutern. In diesem wird erklärt wie man XML-Dateien signiert bzw. deren Signierung validiert.

Gegeben sei dabei folgendes XML:

<?xml version="1.0"?>
<ArrayOfPerson xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Person>
    <Name>Hendrik</Name>
    <Age>26</Age>
  </Person>
  <Person>
    <Name>Paul</Name>
    <Age>16</Age>
  </Person>
</ArrayOfPerson>

Zunächst wird die Datei geladen und in einem Objekt der SignedXml Klasse übergeben. Wichtig ist dabei darauf zu achten ob die Property PreserveWhitspace vor dem Laden gesetzt ist, was sie normalerweise nicht ist. Steht sie auf false werden alle nicht benötigten Whitspaces, wie Tabulatoren die der Formatierung dienen, entfernt. Da diese Whitspaces aber in die Berechnung der Signierung eingehen würde ein nicht gesetztes Flag bedeuten, dass wir das XML verändern wodurch sich auch die Signierung ändert. Dies führt vorallem dann zu Problemen wenn die Signierung nicht von einem in .NET geschriebenen Programm geprüft oder erstellt wird.

XmlDocument xmlDoc = new XmlDocument();
xmlDoc.PreserveWhitespace = true;
xmlDoc.Load(_filePath);

SignedXml signedXml = new SignedXml(xmlDoc);

Die nächste Stelle ist weitestgehend bekannt. Da die SigningKey Property nur Objekte vom Typ AssymetricAlgorithm akzeptiert ist es demnach mit der hier beschriebenen Methode aktuell nur möglich XML per RSA oder DAS zu signieren. Wobei es sonst auch keine anerkannten Verfahren gibt…

DSACryptoServiceProvider serviceProvider = new DSACryptoServiceProvider();
serviceProvider.ImportParameters(KeyParameter);
signedXml.SigningKey = serviceProvider;

Die Referenz enthält laut W3C Spezifikation Informationen die zur Validierung der Signatur gebraucht werden. Da nicht gesagt werden kann welchen Umfang eine zu signierende Datei hat und da assymetrische Verfahren sehr aufwendig sind, wird ein Hashwert berechnet welcher dann signiert wird. Dieser Hashwert wird in der Property DigestValue hinterlegt und ist unter selben Namen im späteren XML zu finden. Die URI Eigenschaft ist in diesem Fall auf einen Leerstring gesetzt, was bedeutet, dass das gesamte eingelesene Dokument zu signieren ist. Hier nicht zu sehen aber automatisch durch SignedXml gesetzt wird der verwendete Hashingalgorithmus welcher später im Referenzbereich des XML auftaucht.

Reference reference = new Reference();
reference.Uri = "";

var env = new XmlDsigEnvelopedSignatureTransform();
reference.AddTransform(env);
signedXml.AddReference(reference);

Eine weitere Information die die Referenz benötigt ist die Vorgehensweise bei der Signierung bzw. in diesem Kontext Transformation genannt. Es gibt verschiedene Transformationen deren Klassennamen immer auf Transform enden und die sich in drei Kategorien unterteilen:

  • Detached – die Signatur befindet sich in einem XML welches selbst nicht signiert wird sondern nur an die zu signierenden Daten angehangen bzw. mitgeliefert wird.
  • Enveloped – Zu signierende Daten und Signatur sind im selben Dokument enthalten, wobei Bestandteile der Signierung selbst (Hash, Algorithmus, Referenz) nicht mit signiert werden.
  • Enveloping – Wie enveloped mit dem Unterschied, dass die dort beschriebenen Ausnahmen inkludiert sind – Ausnahme ist in jedem Fall die Signierung selbst. Würde sie sich selbst in die Berechnung einbeziehen hätte man wohl eine Endlosschleife, wie man das auch immer machen will…
signedXml.ComputeSignature();
XmlElement xmlDigitalSignature = signedXml.GetXml();

XmlNode importedNode = xmlDoc.ImportNode(xmlDigitalSignature, true);
xmlDoc.DocumentElement.AppendChild(importedNode);

Zum Schluss wird die entsprechende Signatur erzeugt und an das bekannte XML-Dokument angehangen. Das Ergebnis des ganzen sieht man im Folgenden:

<?xml version="1.0"?>
<ArrayOfPerson xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Person>
    <Name>Hendrik</Name>
    <Age>26</Age>
  </Person>
  <Person>
    <Name>Paul</Name>
    <Age>16</Age>
  </Person>
  <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#dsa-sha1" />
      <Reference URI="">
        <Transforms>
          <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
        </Transforms>
        <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
        <DigestValue>IZhMF9o1XMA37Md4SGliJDheY98=</DigestValue>
      </Reference>
    </SignedInfo>
    <SignatureValue>
     Gykl3cUvRg8R5y1mE3fCkrYnvNooD7DWCUtqjD6YvBWixifFT9nSKw==
    </SignatureValue>
  </Signature>
</ArrayOfPerson>

Wie man eine XML-Signierung validiert erkläre ich nicht, dafür gibt es das MSDN. Wichtig ist nur noch mal zu sagen, dass man unbedingt beim Einlesen des XML auf die PreserveWhitspace Property achten muss. Falsche Handhabung hier, führt automatisch zu einer langen Sucherei ohne Anhaltspunkte.

Zum Abschluss noch einmal der gesamte Quelltext der Methode zum Signieren:

public void SignXML(string path)
{
  XmlDocument xmlDoc = new XmlDocument();
  xmlDoc.Load(path);

  SignedXml signedXml = new SignedXml(xmlDoc);

  DSACryptoServiceProvider serviceProvider = new DSACryptoServiceProvider();
  serviceProvider.ImportParameters(KeyParameter);
  signedXml.SigningKey = serviceProvider;

  Reference reference = new Reference();
  reference.Uri = "";

  var env = new XmlDsigEnvelopedSignatureTransform();
  reference.AddTransform(env);
  signedXml.AddReference(reference);

  signedXml.ComputeSignature();
  XmlElement xmlDigitalSignature = signedXml.GetXml();

  XmlNode importedNode = xmlDoc.ImportNode(xmlDigitalSignature, true);
  xmlDoc.DocumentElement.AppendChild(importedNode);

  xmlDoc.Save(path);
}

Und hier der Quelltext zur Validierung:

public void CheckSigniature(string filePath)
{
  XmlDocument doc = new XmlDocument();
  doc.Load(filePath);

  SignedXml signedXml = new SignedXml(doc);
  var nodeList = doc.GetElementsByTagName("Signature");

  XmlElement element = nodeList[0] as XmlElement;
  signedXml.LoadXml(element);

  var cryptoServiceProvider = new DSACryptoServiceProvider();
  cryptoServiceProvider.ImportParameters(KeyParameter);
  bool isValid = signedXml.CheckSignature(cryptoServiceProvider);
}