Mathematisches Praktikum - SoSe 2015 Prof. Dr. Wolfgang Dahmen — Felix Gruber, Igor Voulis Aufgabe 6 Bearbeitungszeit: zwei Wochen (bis Freitag, den 17. Juli 2015) Mathematischer Hintergrund: Public-Key-Kryptosysteme, RSA-Algorithmus, erweiterter Euklidischer Algorithmus, Primzahltests Elemente von C++: Klassen, Ein- und Ausgabeoperatoren, Standard Template Library (STL) Public-Key-Verfahren Moderne kryptographische Methoden finden im heutigen Alltag viele Anwendungen. Sie werden beispielsweise bei der Zugangskontrolle zu Computernetzen, zur Geheimhaltung von Daten oder zum Signieren elektronischer Dokumente benutzt. Die Standardanwendung der Kryptographie sind die Verschlüsselungsverfahren zur Geheimhaltung von Daten. Man unterscheidet zwischen symmetrischen und asymmetrischen Verfahren. Grundsätzlich schickt immer Alice (A) eine Nachricht an Bob (B), die sie mit einem Schlüssel π (encryption) unter einem gegebenen Verschlüsselungsverfahren verschlüsselt. Bob wiederum entschlüsselt diese Nachricht mit dem dafür vorgesehenen Schlüssel π (decryption). Sind diese beiden Schlüssel gleich, oder zumindest der eine leicht aus dem anderen zu berechnen, so handelt es sich um ein symmetrisches Verfahren, anderenfalls um ein asymmetrisches Verfahren. Bei einem symmetrischen Verfahren müssen Alice und Bob zur Kommunikation über einen sicheren Kanal den Schlüssel austauschen, was eines der zentralen Probleme dieser Art von Verfahren darstellt. Denkt man an mehrere Benutzer, die allesamt Daten sicher miteinander austauschen wollen, (οΈπ)οΈso muss jeder mit jedem einen sicheren Schlüssel vereinbaren. Bei π Teilnehmern sind das insgesamt schon 2 Schlüssel, die zusätzlich auch noch sicher abgelegt sein müssen. Einen Ausweg stellen die so genannten Public-Key-Verfahren dar, bei denen ein Schlüssel π allgemein bekannt ist (public key) und nur der geheime Schlüssel π (private key) sicher verwahrt werden muss. Jeder, insbesondere Alice, kann dann mit Hilfe von Bobs öffentlichem Schlüssel ππ΅ eine Nachricht an ihn verschlüsseln, die aber nur Bob wieder entschlüsseln kann, da nur er seinen privaten Schlüssel ππ΅ kennt. Der geheime Austausch entfällt ebenso wie die Verwaltung einer sehr großen Menge von Schlüsseln. Bei dieser Art Verfahren muss allerdings sichergestellt sein, dass der öffentliche Schlüssel von Bob auch von Bob ist und nicht von einem unbekannten Dritten ausgetauscht wurde. Hierzu dient eine Public-Key-Infrastruktur, zu der beispielsweise eine Certification-Authority (CA) gehört, die garantiert, dass die verwahrten öffentlichen Schlüssel von den zugehörigen Personen stammen. Solche CAs gibt es mittlerweile sowohl bei öffentlichen Trägern (RWTH Aachen1 , Bund) als auch bei privaten Firmen. Eine dezentrale Aternative zu CAs bietet ein Web of Trust, wie es zum Beispiel von OpenPGP verwendet wird. Leider sind die Public-Key-Verfahren in der Regel nicht so effizient wie viele der symmetrischen Verfahren, so dass man bei großen Datenmengen hybride Verfahren anwendet. Dazu erzeugt Alice einen Sitzungsschlüssel und verschlüsselt damit ihre Daten mit Hilfe eines symmetrischen Verfahrens. Dann verschlüsselt sie diesen Sitzungsschlüssel mit einem Public-Key-Verfahren mit dem öffentlichen Schlüssel von Bob, und hängt ihn an die verschlüsselten Daten an. Den Sitzungsschlüssel verwirft sie nun bzw. erzeugt zu Beginn der nächsten Kommunikation wieder einen Neuen. Das Public-Key-Verfahren wird hier also nur für wenige Daten verwendet, nämlich für den Schlüssel des symmetrischen Verfahrens. 1 Zertifizierungsstelle der RWTH Aachen: https://doc.itc.rwth-aachen.de/display/CERT/Home 1 Signaturprüfung Im Unterschied zur Datenverschlüsselung geht es bei der Signaturprüfung nicht darum, Daten geheim zu halten, sondern man möchte sicherstellen, dass ein empfangenes Dokument auch vom angegebenen Autor stammt, z.B. von Alice. Ist bei dem asymmetrischen Verfahren unerheblich, ob man mit dem öffentlichen Schlüssel π ver- und mit dem privaten Schlüssel π entschlüsselt oder umgekehrt (das gilt nicht immer), so geht Alice wie folgt vor: Mit Hilfe einer sogenannten kryptographischen Hashfunktion berechnet sie von ihrem Dokument einen charakteristischen Fingerabdruck, auch Signatur genannt, normalerweise eine große Zahl. Bei jeder Veränderung des Textes ändert sich dieser Fingerabdruck. Sie verschlüsselt nun die Signatur mit ihrem privaten Schlüssel und hängt sie an das Dokument an. Ist bekannt, welche Hashfunktion sie benutzt hat2 , so kann Bob die Echtheit des empfangenen Dokumentes folgendermaßen feststellen: er entschlüsselt die angehängte Signatur mit dem öffentlichen Schlüssel von Alice (über eine CA erhältlich) und vergleicht sie mit dem Fingerabdruck des empfangenen Dokumentes. Ihre Aufgabe ist es nun, ein Public-Key-Verfahren für eine Signaturprüfung zu programmieren. Es handelt sich dabei um das erste und wahrscheinlich auch bekannteste Verfahren, das RSA-Verfahren [5]. Es ist benannt nach R. Rivest, A. Shamir und L. Adleman, die es 1977 entwickelten. Mittels einer Hashfunktion und einer Liste von Public-Keys sollen Sie entscheiden, welcher der Beispieltexte authentisch ist und vom Autor stammt und welcher Text gefälscht wurde. Zusätzlich sollen Sie ein Public-Private-Key Paar erzeugen (Primzahlerzeugung) und damit eine selbst geschriebene Gedichtzeile signieren (also den Wert der Hashfunktion verschlüsseln). Wir beschreiben zunächst das RSA-Verfahren und im Anschluss daran die zu benutzende Hashfunktion. Die auftretenden Teilaufgaben werden am Ende des Abschnitts diskutiert. RSA-Verfahren Als erstes erzeugt man den privaten und den öffentlichen Schlüssel. Dazu wählt man zwei zufällige Primzahlen π und π mit π > 2, π > 2 und π ΜΈ= π und berechnet das RSA-Modul π = ππ. Die Eulersche π-Funktion3 hat damit den Wert π(π) = (π − 1)(π − 1). Weiter wählt man als Verschlüsselungsexponenten π eine Zahl, die 1 < π < π(π) und ggT(π, π(π)) = 1 erfüllt4 . Dazu berechnet man den Entschlüsselungsexponenten π unter der Bedingung 1 < π < π(π) und ππ ≡ 1 mod π(π). Diese Zahl π kann mit dem erweiterten Euklidischen Algorithmus ermittelt werden. Der öffentliche Schlüssel ist dann das Paar (π, π) und der private Schlüssel die Zahl π. Verschlüsselt wird eine Zahl π mit 0 ≤ π < π wie folgt π ≡ ππ mod π. Zur Berechnung wird das schnelle Potenzieren benutzt. Entschlüsselt wird eine Nachricht π durch π ≡ ππ mod π. 2 Das ist kein Sicherheitsrisiko und nicht geheim! Die Eulersche π-Funktion π : N → N, π β¦→ #{1 ≤ π ≤ π : ggT(π, π) = 1} gibt die Anzahl der zu π teilerfremden natürlichen Zahlen kleiner oder gleich π an. 4 ggT bedeutet größter gemeinsamer Teiler. 3 2 Das Funktionsprinzip beruht auf der Tatsache, dass für die oben konstruierten Schlüssel des RSA-Verfahrens gilt: (ππ )π ≡ π mod π. Insbesondere ist das Verfahren symmetrisch bezüglich der Verwendung der Schlüssel, d.h. die Rollen von π und π können vertauscht werden. Die Signaturprüfung läßt sich also mit dem oben beschriebenen RSAVerfahren durchführen. Hashfunktion, MD5 Ist Σ ein Alphabet, so ist eine Hashfunktion β eine Abbildung β : Σ* → Σπ , π ∈ Z. Sie bildet Strings beliebiger Länge auf Strings mit fester Länge π ab. Es soll in dieser Übung keine sinnvolle Hashfunktion geschrieben, sondern nur eine existierende, nämlich die MD5-Hashfunktion, benutzt werden. Es handelt sich hierbei um einen Algorithmus, der aus einem Text einen Fingerabdruck der Länge 128 Bit erzeugt. Dazu werden in verschiedenen Durchläufen bitweise Verknüpfungen auf Blöcken innerhalb des Textes erstellt. Eine genau Beschreibung findet sich in [2]. Wir verwenden MD5 da es realtiv einfach ist; jedoch sollte es nicht mehr für kryptographische Anwendungen benutzt werden, da es einzwischen Verfahren gibt, mit denen sich leicht Kollisionen von MD5-Hashes generieren lassen. Als Alternative bietet sich beispielsweise SHA-3 an. Arbeiten wir zur Ver- und Entschlüsselung mit dem Datentyp uint64_t, der 64-bittig ist, so belegt ein Fingerabdruck ein uint64_t-Feld der Länge 2. Diese Funktionalität ist in der Klasse Signatur gekapselt, und Objekte dieses Typs werden als Argument der Testroutinen benutzt. Eine Beschreibung der Klasse finden Sie am Ende dieses Dokuments. Ihre Aufgabe ist es nun, bei einem empfangenen Text, der auf Authentizität untersucht werden soll, einen solchen Fingerabdruck zu entschlüsseln und mit dem Fingerabdruck des Textes zu vergleichen, oder bei einem zu sendenden Text einen mit Ihrem Schlüssel verschlüsselten Fingerabdruck mitzuschicken. Euklidischer Algorithmus und Erweiterter Euklidischer Algorithmus Der Euklidische Algorithmus berechnet den größten gemeinsamen Teiler ggT(π, π) zweier Zahlen π und π. Der Algorithmus beruht auf folgender Eigenschaft: Ist π = 0, so ist ggT(π, π) = |π|. Anderenfalls gilt ggT(π, π) =ggT(|π|, π mod π). Schreiben Sie eine Funktion ggt mit folgendem Interface uint64_t ggt(uint64_t a, uint64_t b); welche den ggT(π, π) für Zahlen π und π berechnet und zurückgibt. Der erweiterte Euklidische Algorithmus berechnet darüber hinaus zwei Zahlen π₯, π¦ ∈ Z, so dass gilt5 : ggT(π, π) = π₯π + π¦π. Hierfür werden Folgen (π₯π ),(π¦π ),(ππ ) und (ππ ) konstruiert, bis für ein π ≥ 2 der Rest ππ+1 = 0 wird. Der Term ππ entspricht dann dem ggT(π, π), und die gesuchten Werte für π₯ und π¦ lauten π₯ = (−1)π π₯π und 5 Man kann zeigen, dass solche ganzen Zahlen π₯ und π¦ immer existieren. 3 π¦ = (−1)π+1 π¦π . Die Rekursion wird beschrieben durch ππ = (−1)π π₯π π + (−1)π+1 π¦π π ⌋οΈ ⌊οΈ ππ−1 ππ = ππ β§ βͺ β¨(ππ π₯π + π₯π−1 , ππ π¦π + π¦π−1 ) (π₯π+1 , π¦π+1 ) = (0, 1) βͺ β© (1, 0) 0≤π ≤π+1 1≤π≤π 1≤π≤π π=0 π = −1 Schreiben Sie eine Funktion xggt mit folgendem Interface uint64_t xggt(uint64_t a, uint64_t b, int64_t& x, int64_t& y); welche den Wert ggT(π, π) für Zahlen π und π zurückgibt sowie die Zahlen π₯ und π¦ nach obigem Schema berechnet. Auch wenn mathematisch gesehen eine Folge von Zahlen konstruiert wird, sollten Sie bei der Implementierung der zweistufigen Rekursion das Anlegen von Feldern vermeiden, deren Länge 3 übersteigt. Überlegen sie sich, wie Sie mit Hilfe des obigen erweiterten Euklidischen Algorithmus den Entschlüsselungsexponenten π aus dem RSA-Verfahren berechnen können. Beispiel zum Erweiterten Euklidischen Algorithmus mit π = 120 und π = 25: π ππ ππ π₯π π¦π 0 120 − 1 0 1 25 4 0 1 2 20 1 1 4 3 5 4 1 5 4 0 − 5 24 Daher ist ggT(120, 25) = 5, π = 3, π₯ = −1 und π¦ = 5. Schnelle Potenzberechnung Angenommen es ist auszurechnen: ππ₯ mod π. Weiter sei π₯= π ∑οΈ π₯π 2π π=0 die Binärentwicklung von π₯. Dabei ist π₯π ∈ {0, 1} für alle 0 ≤ π ≤ π. Wegen π₯ ∑οΈπ π =π π=0 π₯π 2π = π ∏οΈ π (π2 )π₯π = π=0 ∏οΈ π π2 0≤π≤π,π₯π =1 π kann man das Produkt nun aus der Multiplikation der Quadrate π2 berechnen, für die π₯π = 1 gilt. Bei der Umsetzung ist auszunutzen, dass π+1 π π2 = (π2 )2 π gilt. Die Modulo-Rechnung kann auf jeden Faktor π2 einzeln angewandt werden. Erst dies ermöglicht bei realistischen Größenordnungen von π, π₯ und π die Berechnung des Ausdrucks ππ₯ mod π. 4 Primzahlerzeugung Primzahlen einer gegebenen Größenordnung (in Bits) kann man dadurch erzeugen, dass man das erste und das letzte Bit in einer Darstellung auf 1 setzt, also die Größenordnung sowie die Zahl als ungerade festlegt. Dann füllt man die inneren Bits zufällig mit 0 oder 1 und führt schließlich mit der so entstandenen Zahl einen Primzahltest durch6 . Realistische Größenordnungen sind 500-2000 Bit, d.h. 150-600 Dezimalstellen. Im Rahmen dieser Aufgabe wollen wir uns aber auf kleine Primzahlen (16 Bit) beschränken. Ihre Aufgabe ist es, zwei (kleine!) Primzahlen π und π in der Größenordnung bis 216 − 1 = 65535 zu finden. Primzahltests findet man in der Literatur z.B. unter den Stichworten: β Probedivision, β Sieb des Eratosthenes, β Fermat-Test und β Miller-Rabin-Test. Im Folgenden skizzieren wir die Probedivision. Die beiden letzt genannten Methoden beruhen auf dem kleinen Satz von Fermat bzw. einer Erweiterung und erfordern etwas mehr Realisierungsaufwand. Sie sollen daher nicht mehr Gegenstand dieser Übung sein. Probedivision Um festzustellen, dass eine gegebene Zahl π nicht prim ist, reicht es offenbar aus, mindestens einen Faktor √ 1 < π < π von π zu finden. Umgekehrt ist eine Zahl dann prim, wenn keine der Primzahlen π ≤ π die Zahl π (ohne Rest) teilt. Erzeugen Sie mit dieser Methode sukzessive alle Primzahlen, die kleiner sind als eine gegebene Oberschranke pMax (hier fest als 65536 codiert). Arbeiten Sie dafür auf der bereitgestellten Klasse Prim. Die in der Methode Prim::init() nach und nach zu ermittelnden Primzahlen werden in der Template-Datenstruktur set<T> pMenge gesammelt, wobei für T der in unit.h definierte Datentyp uint64_t verwendet werden soll. Die interne Sortierung der Einträge dieser Menge erfolgt nach dem “<”-Kriterium der Klasse uint64_t, d.h. aufsteigend. Bei der Probedivision für eine Zahl π müssen nun alle ”kleinen“ Elemente π der pMenge, d.h. √ alle π ≤ π, als Divisor getestet werden. Nähere Erläuterungen zum Zugriff auf einzelne Elemente enthält der folgende Abschnitt. Sets und Maps Variablen vom Typ set (Menge) und map (Abbildung) sind assoziative Container, d.h. Objekte, die zum Verwalten anderer Objekte dienen. Beide Datentypen sind als vorgefertigte Template-Klassen in der Standard Template Library (STL) von C++ enthalten. Der Zugriff auf die Daten eines assoziativen Containers erfolgt anhand eines Schlüssels7 . Im Fall eines sets sind Schlüssel und Daten identisch, bei einer map werden diese aber i.a. nicht übereinstimmen. Der Typ des Schlüssels und der Daten ist frei wählbar. Organisiert man beispielsweise ein Adressbuch als map, dient der Name einer Person als Schlüssel (nach diesem wird der Container intern sortiert), während die Angaben zu Wohnort, Straße und Hausnummer als Daten unter dem jeweiligen Schlüssel abgelegt werden. Für die Verwendung solcher Container im Programm definiert man sich am einfachsten mittels typedef einen neuen Datentyp. Mit 6 Bertrand’s Postulat: “Zu jedem π ∈ N existiert eine Primzahl π mit π < π ≤ 2π” sichert das Auffinden zumindest einer Primzahl im untersuchten Intervall. 7 dieser hat nichts mit der Verschlüsselung einer Nachricht zu tun 5 #include <set> #include <map> typedef set<int> mySet; typedef map<string, int> myMap; wird mySet als Menge von int-Zahlen und myMap als Abbildung mit string-Schlüsseln und int-Daten deklariert. Damit kann z.B. ein Lottoschein oder ein Telefonregister realisiert werden: mySet Lottozahlen; myMap TelReg; Lottozahlen.insert(12); Lottozahlen.insert(47); Lottozahlen.insert(18); Lottozahlen.insert(24); Lottozahlen.insert(2); Lottozahlen.insert(33); TelReg["Ron"] = 79825; TelReg["Hermine"] = 22235; TelReg["Harry"] = 40108; cout << TelReg["Hermine"] << endl; Um Container zu durchlaufen, werden Iteratoren benutzt. Iteratoren sind eine Verallgemeinerung von Zeigern. Sie erlauben es, mit verschiedenen Containern auf gleiche Weise zu arbeiten. Die Ausgabe aller Elemente des Lottoscheins bzw. des Telefonregisters erfolgt z.B. über for (mySet::iterator it=Lottozahlen.begin(); it!=Lottozahlen.end(); ++it) cout << *it << " "; for (myMap::iterator it=TelReg.begin(); it!=TelReg.end(); ++it) cout << "Name: " << it->first << ", TelNr: " << it->second << endl; Hier bedeutet it->first (gleichbedeutend mit (*it).first) den Zugriff auf den Schlüssel (in diesem Falle der Name) und it->second den Zugriff auf die Daten (hier die Telefonnummer). Beachten Sie, dass der Zugriff auf ein Map-Element über den operator[] dieses Element erzeugt und in die Map aufnimmt, falls es noch nicht existiert. Möchten sie nur testen, ob ein Schlüssel existiert, benutzen sie die Methode find. Zusammenfassung der Aufgabe Orientieren Sie sich bei der Bearbeitung der Aufgabe am besten an der vorgegebenen Struktur in der Datei rsa.cpp. Hauptpunkte: A) 1) Generieren Sie ein Public-Private-Schlüsselpaar in der Funktion NeueSchluessel einer Klasse RSA (s.u.). Testen sie dieses durch Aufruf von TestSchluessel. Insbesondere werden dort auch die Funktionen Verschluesseln und Entschluesseln aufgerufen und überprüft. 2) Verschicken Sie mit der Routine SchickeNachricht eine beliebige Nachricht, und signieren Sie diese mit Ihrem privaten Schlüssel. 6 B) 1) Lesen Sie die Datei keys.txt mit den öffentlichen Schlüsseln Ihrer Betreuer ein und legen Sie diese in einer Map ab. Verwenden Sie für die Dateioperationen ifstream, für die Schlüssel die Klasse RSA (s.u.) und für die Menge der Namen und Schlüssel die Klasse map mit den Typen string und RSA. 2) Durchlaufen Sie die Map und holen Sie für jeden Betreuer mittels HoleNachricht eine Nachricht und einen Fingerabdruck. Entschlüsseln Sie diesen Fingerabdruck mit Hilfe des öffentlichen Schlüssels aus der zuvor initialisierten Map und vergleichen ihn mit dem Fingerabdruck der Nachricht. Geben Sie Ihr Ergebnis durch Aufruf von PruefeNachricht weiter. Unterpunkte: Schreiben Sie eine Klasse RSA mit den public-Variablen π, π, π und den private-Variablen π, π, π(π). Diese Klasse soll folgende Funktionen besitzen: uint64_t Verschluesseln(const uint64_t& m, bool bPublic=true); uint64_t Entschluesseln(const uint64_t& c, bool bPublic=false); void NeueSchluessel(); Der boolesche Parameter der ersten beiden Funktionen zeigt an, ob der private oder der öffentliche Schlüssel verwendet werden soll. Zur Ver- und Entschlüsselung benutzen Sie das RSA-Verfahren mit den Variablen der Klasse. Sie benötigen folgende Routinen: i) Schnelle Potenzierung. ii) Euklidischer und Erweiterter Euklidischer Algorithmus. iii) Ver- und Entschlüsselung. Weiter soll die Klasse RSA einen Eingabe- und einen Ausgabeoperator besitzen. Eine erste Version mit den Prototypen der Funktionen finden Sie in unit.h. Existierende Hilfskonstrukte Es werden die 64 Bit langen Integertypen uint64_t und int64_t (signed) aus dem Header cstdint verwendet, die mit dem C++11-Standard in C++ eingeführt wurden. Denken Sie also daran das Programm mit der Compileroption -std=c++11 zu übersetzen. Die Klasse Signatur kapselt im Wesentlichen die externen MD5-Routinen. Mit der Klassenfunktion void set(const string& s); können Sie den Fingerabdruck des Textes s in das interne Feld der Länge length() vom Typ uint64_t setzen. Lesen und Setzen des Feldes erfolgt wie üblich über operator[]. Zufallszahlen erhält man mit Hilfe des eingebauten Pseudozufallszahlen-Generators. Dieser ist zu Beginn Ihres Programmes zu initialisieren (z.B. mit srand(time(NULL))). Der Aufruf von rand() liefert dann eine Zufallszahl im Bereich von 0 bis RAND_MAX= 2147483647 (Typ uint64_t). Zum Testen der Funktionen ggt, xggt und fastpow gibt es die Hilfsfunktionen Pruefe_ggt, Pruefe_xggt und Pruefe_fastpow, die Sie jeweils mit ihrem Funktionsnamen als Argument ausfrufen. Die Prüfroutinen testen dann Ihre Funktionen mit verschiedenen Parametern, siehe auch rsa.cpp. 7 Literatur [1] C/C++-Referenz. http://www.cppreference.com/index.html. [2] MD5. http://www.ietf.org/rfc/rfc1321.txt. [3] Buchmann, J.: Einführung in die Kryptographie. Springer Verlag, Heidelberg, 1999. [4] Menezes, A. J., P. C. van Oorschot und S. A. Vanstone: Handbook of Applied Cryptography. CRC Press, Boca Raton, 1996. http://www.cacr.math.uwaterloo.ca/hac/. [5] Rivest, R. L., A. Shamir und L. Adleman: A Method for Obtaining Digital Signatures and Public-Key Cryptosystems. Communications of the ACM, 21(2):120–126, 1978. Preprint: http://people.csail. mit.edu/rivest/Rsapaper.pdf. [6] Schneier, B.: Applied Cryptography. John Wiley & Sons, New York, 1996. 8