Exportieren (0) Drucken
Alle erweitern
Erweitern Minimieren

Arbeiten mit Zertifikaten in .NET 2.0 – Teil 2

Veröffentlicht: 30. Mai 2006
Von Dominick Baier

In „ Arbeiten mit Zertifikaten in .NET 2.0 – Teil 1 “ dieses Artikel-Zweiteilers haben Sie gesehen, wie man prinzipiell mit X.509 Zertifikaten in .NET 2.0 umgeht. Dazu gehörte das Laden einer Datei sowie aus dem Zertifikat-Speicher, Arbeiten mit den Eigenschaften und den Standard - Dialogen sowie die Validierung von ganzen Zertifikats-Ketten. In diesem Teil beleuchten wir die kryptografischen Operationen, die mit einem Zertifikat und dem dazugehörigen privaten Schlüssel durchgeführt werden können.

Auf dieser Seite

 RSA, PKCS und CMS ?
 Signieren von Daten
 Verschlüsseln von Daten
 Entschlüsseln und Prüfen
 Beispiel: Verschlüsseln und Signieren von Dateien
 Zusammenfassung
 Der Autor

RSA, PKCS und CMS ?

Die Firma RSA Security (benannt nach ihren drei Gründern Ron Rivest, Adi Shamir und Len Adleman) ist fest verbandelt mit der Geschichte der asymmetrischen Kryptografie. Neben der Entwicklung des wohl bekanntesten Algorithmus für Public/Private Key Kryptografie mit Namen RSA, sind sie auch Herausgeber mehrerer Standard-Dokumente, die die Implementierung von asymmetrischen Kryptografie-Methoden beschreiben. Diese Dokumente werden allgemein mit Namen PKCS (Public Key Cryptography Standard) referenziert und wurden zuerst im Jahre 1991 veröffentlicht. Die darin enthaltenen Spezifikation haben Einzug in populäre Technologien wie S/MIME oder SSL gehalten.

Ein Dokument aus dieser Reihe – PKCS #7 – beschreibt Standard-Formate für verschlüsselte und signierte Daten. Diese Spezifikationen sind auch als CMS bekannt – Cryptographic Message Syntax. Sie können die offiziellen Standard-Dokumente auf der Website www.rsasecurity.com einsehen.

Der neue System.Security.Cryptography.Pkcs Namensraum in .NET 2.0 implementiert Teile der PKCS Spezifikation, insbesondere PKCS#7 und arbeitet direkt mit den Klassen für X.509 Zertifikate zusammen. Da hier Standards zum Einsatz kommen, sind Ihre mit diesen Klassen geschützten Daten kompatibel zu anderen Systemen, die ebenfalls PKCS#7 implementieren.

Hinweis: Der PKCS#5 Standard (und dabei speziell PBKDF2 zur Erzeugung von Schlüsseln aus Passwörtern) wird in der neuen Klasse Rfc2898DeriveBytes implementiert.

Signieren von Daten

Bevor man Daten verschlüsselt, sollten diese immer zuerst gegen Manipulationen geschützt werden. Da eine digitale Signatur rein technisch nichts anderes ist als ein Hash, der mit einem privaten Schlüssel verschlüsselt wird, ist dafür ein Zertifikat mit einem assoziierten privaten Schlüssel notwendig (z.B. aus einer „.pfx“ Datei oder dem persönlichen Zertifikat-Speicher).

Die folgenden drei Klassen sind an diesem Vorgang beteiligt (die jeweiligen formalen Definitionen dieser Datentypen finden Sie im PKCS#7 Standard unter http://www.rsasecurity.com/rsalabs/node.asp?id=2129):

Klasse

Bedeutung

ContentInfo

Repräsentiert eine PKCS#7/CMS Datenstruktur. Diese Klasse ist die Basis für alle PKCS#7 Operationen.

CmsSigner

Repräsentiert einen Signierer und wird aus einem X509Certificate2 generiert. Entspricht dem SignerInfo Standard-Datentyp.

SignedCms

Repräsentiert signierte Daten. Entspricht dem SignedData Standard-Datentyp.

Um ein Byte Array zu signieren, müssen Sie zuerst ein ContentInfo Objekt erstellen, aus dem wiederum ein SignedCms Objekt erzeugt wird. Das SignedCms Objekt erstellt die digitale Signatur mit Hilfe des CmsSigner Objekts, das das Zertifikate des Signierers repräsentiert.

byte[] Sign(byte[] data, X509Certificate2 signingCert)
{
    // ContentInfo erzeugen
    ContentInfo content = new ContentInfo(data);

    // SignedCms repräsentiert signierte Nachricht
    SignedCms signedMessage = new SignedCms(content);

    // Signierer aus Zertifikat erzeugen
    CmsSigner signer = new CmsSigner(signingCert);

    // Daten signieren
    signedMessage.ComputeSignature(signer);

    // PKCS#7 Format erzeugen
    byte[] signedBytes = signedMessage.Encode();

    // zurückliefern von Daten mit Signatur 
    return signedBytes;
}

Die Encode() Methode erstellt das Standard-Konforme PKCS#7 Format, das die Daten, die Signatur sowie Zertifikats-Daten des Signierers enthält.

Im vorangegangenen Code wird die Signatur fest mit den eigentlichen Daten „verheiratet“. Signaturen erzeugen aber, gerade bei kleinen Daten, einen nicht unerheblichen Overhead. So wächst ein 16 Byte Datum (z.B. ein Kreditkarten Nummer) auf mehr als zwei KB nach der Signatur an. Oftmals ist es aber erwünscht, mehrere getrennte Daten digital zu signieren (und sie damit vor unbemerkten Änderungen zu schützen). Bei Benutzen der eben gezeigten Methode, würde dabei der Platzbedarf (z.B. in einer Datenbank) explodieren.

Für diesen Fall gibt es die Möglichkeit, eine sog. detached Signatur durchzuführen. Dies bedeutet, dass das SignedCms Objekt lediglich die Signatur ohne die eigentlichen Daten zurückliefert. Damit lässt sich eine Signatur über mehrere Werte bilden, die dann getrennt abgespeichert werden kann. Der Code dafür ist sehr ähnlich, Sie geben lediglich ein zusätzliches true beim Erzeugen von SignedCms an.

byte[] SignDetached(X509Certificate2 signingCert, byte[] data)
{
    // ContentInfo erzeugen
    ContentInfo content = new ContentInfo(data);

    // Dieses Mal wird 'true' an den ctor übergeben
    // Dies bedeutet das detached gearbeitet werden soll
    SignedCms signedMessage = new SignedCms(content, true);

    CmsSigner signer = new CmsSigner(signingCert);

    signedMessage.ComputeSignature(signer);

    byte[] signedBytes = signedMessage.Encode();

    // zurückliefern der Signatur (ohne Daten)
    return signedBytes;
}

Hinweis: Alle Kryptografie APIs in .NET arbeiten vorzugsweise mit Byte Arrays. Wenn Sie textuelle Daten verschlüsseln bzw. entschlüsseln möchten, müssen Sie die Strings zuerst zu Byte Arrays kodieren. Dabei hilft die Encoding Klasse aus dem System.Text Namensraum. Wählen Sie die dafür entsprechende Kodierung (z.B. UTF8 oder Unicode) und rufen Sie die Methoden GetString() und GetBytes() auf.

byte[] bytes = Encoding.UTF8.GetBytes(someString);
string someString = Encoding.UTF8.GetString(bytes);

Verschlüsseln von Daten

Der zweite Schritt beim Schützen der Dateien ist die Verschlüsselung. Neben dem bereits bekannten ContentInfo Objekts kommen hier folgende Klassen zum Einsatz:

Klasse

Bedeutung

CmsRecipient

Empfänger der verschlüsselten Daten. Die Daten werden mit dem öffentlichen Schlüssel des „gewrappten“ Zertifikats verschlüsselt. Entspricht dem RecipientInfo Standard-Datentyp.

CmsRecipientCollection

Bei mehreren Empfängern wird eine Collection von diesem Typ gefüllt und übergeben

EnvelopedCms

Repräsentiert verschlüsselte Daten. Entspricht dem EnvelopedData Standard-Datentyp.

Folgender Code verschlüsselt ein Byte Array mit Hilfe des öffentlichen Schlüssels aus dem übergebenen Zertifikat.

byte[] Encrypt(X509Certificate2 encryptingCert, byte[] data)
{
    // ContentInfo erzeugen
    ContentInfo plainContent = new ContentInfo(data);

    // EnvelopedCms repräsentiert verschlüsselte Daten
    EnvelopedCms encryptedMessage = new 
      EnvelopedCms(plainContent);

    // Empfänger hinzufügen
    CmsRecipient recipient = new CmsRecipient(encryptingCert);

    // Nachricht mit öffentlichen Schlüssel des Empfängers 
    // verschlüsseln
    encryptedMessage.Encrypt(recipient);

    // PKCS#7 Format erzeugen
    byte[] encryptedBytes = encryptedMessage.Encode();

    return encryptedBytes;
}

Die Encode() Methode erzeugt wieder das Standard Daten Format. Intern werden die Daten mit einem zufällig generierten Session Key symmetrisch verschlüsselt. Der Session Key wird wiederum pro Empfänger mit dessen öffentlichen Schlüssel verschlüsselt. Zusätzlich werden Informationen über den Empfänger-Schlüssel abgelegt, damit dieser den entsprechenden privaten Schlüssel wieder zuordnen kann.

Entschlüsseln und Prüfen

Am anderen Ende der Kommunikationsbeziehung werden die Daten zuerst entschlüsselt, und danach auf ihre Integrität geprüft.

Da alle benötigten Informationen (z.B. welche öffentlichen und privaten Schlüssel benötigt werden) in dem Datenpaket enthalten sind, gestalten sich diese Operationen ohne großen Verwaltungs-Overhead. Wenn Sie nicht den Zertifikat-Speicher benutzen, können Sie auch manuell die entsprechenden Schlüsselpaare angeben (mehr Informationen dazu finden Sie in der Visual Studio Hilfe für die EnvelopedCms und SignedCms Klassen).

In den meisten Fällen müssen Sie lediglich ein EnvelopedCms Objekt erzeugen, die Decrypt() Methode aufrufen und die Klartext-Bytes extrahieren.

static byte[] Decrypt(byte[] data)
{
    // EnvelopedCms erzeugen und Daten deserialisieren
    EnvelopedCms encryptedMessage = new EnvelopedCms();
    encryptedMessage.Decode(data);

    // Daten entschlüsseln
    encryptedMessage.Decrypt();

    return encryptedMessage.ContentInfo.Content;
}

Die Integritäts-Prüfung der Daten besteht logisch aus zwei Schritten. Es muss überprüft werden, ob die Signatur intakt ist, was gewährleistet, dass die Daten nicht nach dem Signieren verändert wurden. Weiterhin muss aber auch das Zertifikat des Signierers auf Gültigkeit und Vertrauenswürdigkeit geprüft werden.

Die CheckSignature() Methode der SignedCms Klasse kann beides leisten. Wenn Sie allerdings mehr Kontrolle über diesen Prozess benötigen, können Sie die Signatur- und Zertifikats-Validierung entkoppeln und mit der X509Chain Klasse, die im vorangegangenen Teil behandelt wurde, arbeiten.

Nachfolgende Routine überprüft die Signatur sowie das Zertifikat und gibt die Klartext-Daten ohne angeheftete Signatur zurück. Über die SignerInfos Collection erhält man Zugriff auf die Signatur-Zertifikate.

byte[] VerifyAndRemoveSignature(byte[] data)
{
    // SignedCms erzeugen und daten deserialisieren
    SignedCms signedMessage = new SignedCms();
    signedMessage.Decode(data);

    // false bedeutet das Signatur und Zertifikat
    // geprüft werden. Tritt dabei ein Fehler auf,
    // wird eine Exception erzeugt.
    signedMessage.CheckSignature(false);

    // Zugriff auf das Signatur-Zertifikate
    foreach (SignerInfo signer in signedMessage.SignerInfos)
    {
        Console.WriteLine("Signiert von: {0}", 
          signer.Certificate.Subject);
    }

    // Zurückgeben der Daten ohne Signatur
    return signedMessage.ContentInfo.Content;
}

Bei detached Signaturen muss wieder lediglich ein zusätzlicher Parameter im SignedCms Konstruktor verwendet werden. Folgende Routine prüft eine detached Signatur und liefert einen boolschen Wert zurück.

static bool VerifyDetached(byte[] data, byte[] signature)
{
    ContentInfo content = new ContentInfo(data);

    // Wiederum wird hier true übergeben für detached
    SignedCms signedMessage = new SignedCms(content, true);

    // deserialisieren der Signatur
    signedMessage.Decode(signature);

    try
    {
        // Überprüfen ob Daten und Signatur zusammenpassen
        // Auch hier wird im gleichen Schritt auch
        // das Zertifikat überprüft.
        signedMessage.CheckSignature(false);
    }
    catch
    {
        return false;
    }

    return true;
}

Beispiel: Verschlüsseln und Signieren von Dateien

Als praktische Anwendung dieser Klassen wollen wir eine kleine Anwendung schreiben, die Dateien verschlüsseln und digital signieren kann, bzw. diese Signaturen prüfen und die Daten wieder entschlüsselt. In Abbildung 1 sehen Sie die Oberfläche der Anwendung.

CrypterPK Beispiel Anwendung
Abbildung 1 – CrypterPK Beispiel Anwendung

Sie können ein Signatur- sowie mehrere Verschlüsselungs-Zertifikate angeben. Die Datei kann von jedem ausgewählten Empfänger wieder entschlüsselt werden.

Den Code hinter den Auswahl-Schaltflächen überlasse ich Ihnen als Übung – es werden die APIs aus dem vorangegangenen Teil dieses Artikels benutzt, um Zertifikate aus dem Zertifikat-Speicher auszuwählen. Der eigentlich interessante Teil ist der Code hinter den Verschlüsseln/Entschlüsseln Buttons.

Die Verschlüsselungs-Routine liest die zu schützenden Datei ein, benutzt die zuvor vorgestellten Methoden um den Datei-Inhalt zu signieren und zu verschlüsseln und überschreibt abschließend die Original Datei mit dem neuen Inhalt. Signierer und Empfänger sind in den Variablen _signer und _recipients respektive gespeichert.

void SignAndEncrypt(string filename)
{
    // Temporären Dateinamen erzeugen
    string tempFile = Path.GetTempFileName();
    byte[] file = null;
    // Original Datei einlesen
    using (BinaryReader reader = 
      new BinaryReader(new FileStream(
      filename, FileMode.Open, FileAccess.Read, FileShare.Read)))
    {
        file = new byte[reader.BaseStream.Length];
        reader.Read(file, 0, file.Length);
    }

    // Signieren
    byte[] signedFile = Sign(file, _signer);

    // Verschlüsseln
    byte[] signedAndEncryptedFile = 
      Encrypt(signedFile, _recipients);
    
    // Neue Datei schreiben
    using (BinaryWriter writer = 
      new BinaryWriter(new FileStream(
      tempFile, FileMode.Create, FileAccess.Write, 
      FileShare.None)))
    {
        writer.Write(signedAndEncryptedFile);
    }

    // Dateien austauschen
    File.Delete(filename);
    File.Move(tempFile, filename);
}

Die Decrypt() Methode kehrt den ganzen Vorgang um und schreibt die Datei wieder im Klartext auf die Festplatte.

public void DecryptAndVerify(string filename)
{
    byte[] file = null;
    string tempFile = Path.GetTempFileName();
    using (BinaryReader reader = 
    new BinaryReader(new FileStream(
    filename, FileMode.Open, FileAccess.Read, FileShare.None)))
    {
        file = new byte[reader.BaseStream.Length];
        reader.Read(file, 0, file.Length);
    }

    // Entschlüsseln
    byte[] decryptedFile = Decrypt(file);

    // Signatur und Zertifikate prüfen
    byte[] decryptedAndVerifiedFile = 
      VerifyAndRemoveSignature(decryptedFile);
    using (BinaryWriter writer = 
    new BinaryWriter(new FileStream(
    tempFile, FileMode.Create, FileAccess.Write)))
    {
        writer.Write(decryptedAndVerifiedFile);
    }

    File.Delete(filename);
    File.Move(tempFile, filename);
}

Zusammenfassung

Dank der Implementierung des PKCS#7 Standards können wir nun mit .NET 2.0 interoperable verschlüsselte und signierte Datenpakete erzeugen. In diesem Teil haben Sie gesehen, wie Sie die EnvelopedCms und SignedCms Klassen einsetzen können, und zusammen mit den allgemeinen Zertifikat-Verwaltungs APIs aus Teil 1 dieses Artikels, lassen sich damit richtige zertifikat-basierte Sicherheitssysteme implementieren.

Der Autor

Dominick Baier entwickelt ASP.NET- und Security-Kurse bei DevelopMentor, einer Entwickler-Schulungsfirma. Darüber hinaus unterstützt er Unternehmen beim Schreiben von sicheren verteilten Anwendungen. Als Referent kann man ihn auf diversen Konferenzen antreffen (BASTA, WinDev, DevWeek u.a.); Online findet man ihn auf seinem Blog unter www.leastprivilege.com.

Artikel-Serie „Arbeiten mit Zertifikaten in .NET 2.0“ im Überblick:

Teil 1
http://www.microsoft.com/germany/msdn/library/security/ArbeitenmitZertifikateninNET20Teil1.mspx

Teil 2
http://www.microsoft.com/germany/msdn/library/security/ArbeitenmitZertifikateninNET20Teil2.mspx


Microsoft führt eine Onlineumfrage durch, um Ihre Meinung zur MSDN-Website zu erfahren. Wenn Sie sich zur Teilnahme entscheiden, wird Ihnen die Onlineumfrage angezeigt, sobald Sie die MSDN-Website verlassen.

Möchten Sie an der Umfrage teilnehmen?
Anzeigen:
© 2015 Microsoft