%% LyX 1.6.0 created this file.  For more info, see http://www.lyx.org/.
%% Do not edit unless you really know what you are doing.
\documentclass[ngerman]{article}
\usepackage[T1]{fontenc}
\usepackage[utf8x]{inputenc}
\usepackage{babel}

\usepackage{url}
\usepackage[unicode=true, pdfusetitle,
 bookmarks=true,bookmarksnumbered=false,bookmarksopen=false,
 breaklinks=false,pdfborder={0 0 1},backref=false,colorlinks=false]
 {hyperref}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Textclass specific LaTeX commands.
\newenvironment{lyxlist}[1]
{\begin{list}{}
{\settowidth{\labelwidth}{#1}
 \setlength{\leftmargin}{\labelwidth}
 \addtolength{\leftmargin}{\labelsep}
 \renewcommand{\makelabel}[1]{##1\hfil}}}
{\end{list}}
\newenvironment{lyxcode}
{\par\begin{list}{}{
\setlength{\rightmargin}{\leftmargin}
\setlength{\listparindent}{0pt}% needed for AMS classes
\raggedright
\setlength{\itemsep}{0pt}
\setlength{\parsep}{0pt}
\normalfont\ttfamily}%
 \item[]}
{\end{list}}

\begin{document}

\title{Wurstkessel — Kryptographie in Forth}
\ifx\shorttitle\undefined\else
\shorttitle{Wurstkessel — Forth--Kryptographie}
\fi


\author{Bernd Paysan}
\maketitle
\begin{abstract}
Dieser Artikel behandelt das Thema ,,symmetrische Verschlüsselung{}``
anhand eines Beispiels. Zur symmetrischen Verschlüsselung gehört auch
ein kryptographisch starker Hash und ein ebensolcher Zufallsgenerator.
Und da man Programme, die man schreibt, auch testen muss, können Überlegungen
zum Knacken der Verschlüsselung auch nicht fehlen.
\end{abstract}
\begin{multicols}{2}


\section*{Motivation}

Auf die Idee, einen eigenen Algorithmus zur symmetrischen Verschlüsselung
zu entwickeln, kam ich, als ich die Implementierung des ,,Whirlpool{}``-Algorithmus
\cite{whirlpool} angesehen habe. Whirlpool ist kein Haushaltsgerät,
sondern ein relativ neuer Hash--Algorithmus. MD5 ist ja geknackt,
und damit wertlos. Auch SHA--1 hat bekannte Schwächen, und wenn einmal
eine Schwäche bekannt ist, dann taucht bald die nächste auf \cite{sha-1}.
Und SHA--2 basiert auf demselben Algorithmus, ist also wohl auch nicht
besser. Also müssen zumindest neue Hash--Algorithmen her.

Als Ziel habe ich mir dann gesetzt, dass der Algorithmus einfach implementiert
werden kann, sowohl in Hardware als auch in Software --- und da auch
auf sehr einfach gestrickten CPUs --- und dass er ein inhärent hohes
Maß an Parallelität hat, und damit zumindest theoretisch auch ein
hoher Durchsatz möglich ist. Und natürlich sollte der Algorithmus
auch skalieren, d.h. es soll möglich sein, die Schlüssel- bzw. Hashlänge
je nach Bedarf zu variieren.

Die aktuelle Runde der NIST, einen Kandidaten für SHA--3 zu finden,
habe ich zwar verpasst. Das ist aber nicht wirklich schlimm, denn
Kryptographie gedeiht nicht mit Schnellschüssen.


\section{Kryptologische Grundlagen}

In der VD 1/2005 habe ich ja schon mal über Verschlüsselung geschrieben,
damals über den RSA--Algorithmus, der zum Austausch von Schlüsseln
und zur digitalen Signatur verwendet wird. Damals ging es um asymmetrische
Verschlüsselung, deren Hauptzweck der Austausch von Schlüsseln (Public--Key--Verfahren)
und digitale Signaturen sind. Das damals war nur eine schematische
Modellimplementierung, der zur Anwendung noch einige Details fehlten,
neben der Bignum--Bibliothek insbesondere
\begin{itemize}
\item ein guter Pseudo--Zufall
\item ein symmetrisches Verschlüsselungsverfahren
\item und ein kryptographischer Hash
\end{itemize}
Wir werden sehen, dass die drei Verfahren auf derselben Basis aufbauen
können. Gerade der Zufall ist ein wichtiges Element, denn wenn man
eine wirklich gute Zufallsfolge hat, kann man mit ihr auch perfekt
verschlüsseln. Der sogenannte ,,One--Time--Pad{}`` ist die einzige
kryptographische Methode, die wirklich nicht brechbar ist --- wenn
man sie bestimmungsgemäß anwendet, d.h. den zu verschlüsselnden Text
mit einer Folge von Zufallszahlen verknüpft, die mindestens so lang
wie der Text ist, und genau einmal gebraucht wird. In der Praxis ist
diese Methode nur in Spezialfällen brauchbar, da der Aufwand zum sicheren
Austausch des Schlüssels genauso hoch ist wie der Aufwand zum geheimen
Austausch einer unverschlüsselten Nachricht --- und ein vorab ausgetauschter
Schlüssel dann auch noch entsprechend sicher verwahrt werden muss.

Trotzdem gibt uns diese Methode ein Ziel vor: Unsere Verschlüsselung
soll sich ohne Kenntnis des Schlüssels nicht von Zufall unterscheiden,
und der Schlüssel selbst darf nur durch Ausprobieren gefunden werden.
Etwas Vergleichbares gilt auch für einen Hash: Der Hash--Wert einer
Nachricht ist zwar deterministisch durch die Nachricht bestimmt, soll
aber ebenfalls wie ein völlig zufälliger Wert aussehen, der sich zwar
mit jeder Änderung der Nachricht ändert, aber in einer unvorhersehbaren
Weise --- ändert man ein Bit in der Nachricht, so sollten sich ca.
die Hälfte aller Bits des Hashes andern (und natürlich nicht vorhersehbar
sein, welche Hälfte der Bits sich ändert). Aus dem Hash--Wert soll
nicht auf die Nachricht zurückgeschlossen werden können; genauso wenig
soll es möglich sein, durch gezielte Modifikation der Nachricht einen
bestimmten Hashwert zu erzeugen.

Whirlpool verwendet zwar einen anderen theoretischen Ansatz, die Implementierung
für 64--Bit--Prozessoren ist aber tabellengetrieben. Aus genau diesem
tabellengetriebenen Ansatz habe ich dann meinen Algorithmus hergeleitet.
Basis ist eine Funktion, die zwei 512 Bit lange Zahlen so durch den
Fleischwolf dreht, dass nur noch Wurst herauskommen kann:

\[
\phi(a,s,e)\to a',s'\]


wobei
\begin{lyxlist}{00.00.0000}
\item [{$a$}] ein Akkumulator ist,

\begin{itemize}
\item der am Ende der Transformation als Hashwert ausgegeben wird,
\item bei der symmetrischen Verschlüsselung mit dem Text verknüpft wird,
\item oder als Zufallszahl dient,
\end{itemize}
\item [{$s$}] ein interner Zustand, der nicht preisgegeben werden soll,
und
\item [{$e$}] eine Entropiequelle, also bei Hash und Verschlüsselung die
Botschaft selbst, beim Generieren von Zufallszahlen externe, echt
zufällige Ereignisse, die mit dem Pseudozufallsgenerator gemischt
werden können.
\end{lyxlist}
Die Anforderung an $\phi$ ist schlicht: Sie soll sich leicht und
schnell berechnen lassen, aber die Umkehrung $\phi^{-1}$ soll praktisch
unmöglich berechenbar sein.


\section{Algorithmus}

Ich will also aus Eingangsvektoren möglichst schnell möglichst ,,zufällige{}``
Zahlen bekommen. Was liegt näher, als die Eingangsvektoren über eine
Tabelle einfach auf wirklich zufällige Werte abzubilden? Ich habe
mir auf \url{www.random.org} also 256 wirklich zufällige 64--Bit--Werte
besorgt. Die Indices in diese Tabelle sind also Bytes aus meinem Akkumulator
und dem internen Zustand. Diese 64--Bit--Werte verknüpfe ich mit einem
XOR--Operator; damit Vertauschungen auch unterschiedliche Werte geben,
rotiere ich den Wert vor der nächsten XOR--Verknüpfung um eins nach
links.

Die Idee dabei ist: Indizierung von 256 zufälligen 64--Bit--Werten
ergibt eben diese 256 möglichen zufälligen Werte. Verknüpfe ich zwei
davon, einer um ein Bit rotiert, bekomme ich $256^{2}=64$k Werte,
zwischen denen kein erkennbarer Zusammenhang besteht. Acht Werte derart
verknüpft, ergibt ca. $256^{8}=2^{64}$ verschiedene Werte, zwischen
denen ebenfalls kein erkennbarer Zusammenhang besteht. Wenn die Theorie
stimmt, dann sollten dabei $2^{32}$ Kollisionen entstanden sein,
d.h., bestimmte Ausgangswerte liefern das gleiche Ergebnis. Das ist
so gewünscht, das würde nämlich auch passieren, wenn man statt der
Abbildung einfach $2^{64}$ echte Zufallszahlen ziehen würde.

Damit das Ganze noch weniger deterministisch wird, starte ich diese
Verknüpfung mit einem 64--Bit--Wert aus dem Status; dabei permutiere
ich die Status--Wörter in einem möglichst langen Zyklus.

Den Index in die Tabelle bekomme ich, indem ich je ein Byte aus dem
Status mit einem Byte aus dem Message--Akkumulator verXODERe. Dabei
transponiere ich den Status, d.h., ich schreibe die Wörter byteweise
in Zeilen und laufe dann spaltenweise durch. Den Message--Akkumulator
durchlaufe ich mit Primzahlschritten, jede Runde eine andere Schrittweite.
Am Ende einer Runde wird der alte Status mit dem Message--Akkumulator
ver-XODERt. Damit stelle ich sicher, dass selbst für den Fall einer
Kollision entweder der Message--Akkumulator oder der Status einen
anderen Wert enthält, also bei der nächsten Runde wieder ein anderes
Ergebnis gefunden wird.

Oder in mathematische Formelsprache gekleidet:
\begin{description}
\item [{$rng_{x}$}] 256 64--bittige Zufallszahlen von \url{www.random.org}
in einer Tabelle
\item [{$rol(rng_{x},1)\oplus rng_{y}$}] gibt 64k zufällig aussehende
Zahlen
\item [{$r^{j}=\bigoplus_{i=0}^{7}rol(rng_{x_{i}^{j}},i)$}] gibt ca. $2^{64}-2^{32}$
zufällig aussehende Zahlen
\item [{$x_{i}^{j}=a_{p(8j+i)}\oplus s_{j}^{i}$}] Index in die Tabelle:
Bytes aus dem Akkumulator (in verschiedenen Schrittweiten durchlaufen)
mit einem Byte des transponierten Status verknüpfen
\item [{$s^{j}=s^{p(j)}\oplus r^{j}$}] Die 64--Bit--Worte des Status werden
permutiert und mit den generierten Zahlen verXODERt.
\item [{$e'=a\oplus e,a'=s\oplus e'$}] Akkumulator, Message (,,Entropie{}``)
und Status miteinander verXODERn.
\end{description}
Die ,,naive{}`` Implementierung in Forth sieht dann so aus (nur
die wesentlichen Teile):
\begin{lyxcode}
8table~round\#~\\
~~~13~,~29~,~19~,~23~,~31~,~47~,~17~,~37~,~\\
8table~permut\#~\\
~~~2~,~6~,~1~,~4~,~7~,~0~,~5~,~3~,~\\
~~~\textbackslash{}~permut~length~15~\\
:~mix2bytes

~~(~index~n~k~-{}-~b1~..~b8~index'~n~)~\\
~~wurst-state~+~8~0~DO~\\
~~~~>r~over~wurst-source~+~c@~\\
~~~~r@~c@~xor~-rot~dup~>r~+~\$3F~and~\\
~~~~r>~r>~8~+~LOOP~\\
~~drop~;~\\
:~bytes2sum~(~ud~b1~..~b8~-{}-~ud'~)~\\
~~~~~>r~>r~>r~>r~~>r~>r~>r~>r~\\
~~~~~r>~rngs~wurst~~r>~rngs~wurst~\\
~~~~~r>~rngs~wurst~~r>~rngs~wurst~\\
~~~~~r>~rngs~wurst~~r>~rngs~wurst~\\
~~~~~r>~rngs~wurst~~r>~rngs~wurst~;~~\\
:~round~(~n~-{}-~)~dup~1-~swap~~8~0~DO~\\
~	wurst-state~I~permut\#~\\
~~~~~64s~+~64@~-64swap~\\
~	I~mix2bytes~2>r~bytes2sum~2r>~\\
~~~~~64swap~nextstate~I~64s~+~64!~\\
~~~~~LOOP~2drop~update-state~;~~\\
:~rounds~(~n~-{}-~)~~message~swap~\\
~~~~dup~\$F~and~8~umin~0~?DO~\\
	I~rounds\#~round~\\
	dup~'round-flags~Ith~and~IF~\\
	~~~~swap~+entropy~swap~\\
	THEN~\\
~~~~LOOP~2drop~;~
\end{lyxcode}

\subsection{Performance}

Die naive Implementierung war zunächst sehr langsam (bigForth: ca.
1MB/s auf einem 2,5GHz Phenom). Wahrscheinlich war ein Code--Daten--Konflikt
das Problem --- nach Beseitigung dessen ging die Performance auf 23MB/s
hoch (Gforth: 20MB/s, dank 64 Bit nur unwesentlich langsamer). Um
die Performance zu verbessern, habe ich die Schleife aufgerollt, was
die Performance auf 46MB/s verdoppelte (bigForth).

Das war immer noch nicht sehr befriedigend, also habe ich das libcc--basierte
C--Interface in Gforth genutzt, um den aufgerollten Schleifen--Code
mit dem GCC zu compilieren. Mit knapp über 500MB/s kam dabei dann
ein Ergebnis heraus, das auch den Erwartungen entspricht. Als Benchmark
nutze ich das Wort \texttt{time-rng}, als Berechnungsgrundlage dient
eine Runde pro 512 Bits. Der Zufallsgenerator, der hier getimed wird,
macht zwei Runden pro 512 Bits, liefert also ca. 250MB/s Zufallszahlen.


\section{Analyse}

Um festzustellen, wie sicher der Algorithmus ist, muss man natürlich
versuchen, ihn zu brechen. Die Funktion $\phi$ soll ja unumkehrbar
sein, d.h., wir sollten uns ansehen, wie sich z.B. das Ändern eines
einzelnen Bits auswirkt. Nach einer Runde unterscheidet sich ein 64--Bit--Wort,
und dieses 64--Bit--Wort hat auch nur genau einen unserer 256 Zufallswerte
getauscht. Das heißt, wir müssen uns ,,nur{}`` die 64k möglichen
Kombinationen zwischen zwei Zufallswerten und 8 mögliche Rotationen
ansehen, um auf die Änderung dieses einzelnen Bits zurückschließen
zu können.

Bei der nächsten Runde breitet sich die Änderung des Status entsprehend
aus. Alle 8 Status--Worte sind nun anders und haben mindestens eine
Zufallszahl getauscht; da wir in den meisten Fällen auch Auswirkungen
auf die Änderung des Message--Akkumulators haben, im Durchschnitt
sogar zwei Zufallszwahlen. Dazu müssten wir nun $2^{32}$ mögliche
Kombinationen durchsuchen, und zwar pro Wort. Eine weitere Runde sind
dann alle Zufallszahlen im Status ausgetauscht, ein Zurückschließen
ist weitgehend unmöglich. Der Message--Akkumulator hinkt prinzipbedingt
immer eine Runde hinterher, weil hier nur der alte Status ver-XODERt
wird. Wir hängen also noch eine vierte Runde an, um sicher zu gehen,
dass sowohl Status als auch Message--Akkumulator ausreichend durcheinandergewürfelt
sind.

Wie viele Runden müssen wir zwischen dem Hinzufügen von
Information beim Hashen ausführen? Vermutlich nur eine. Wir müssen
aber am Ende der Hash--Berechnung mindestens 4 Leerrunden durchführen,
damit kein Rückschluss auf die Message möglich ist.

Wieviele müssen wir beim Verschlüsseln ausführen? Mindestens 2. Um
das zu beweisen, reicht eine einfache Klartextattacke: Angenommen,
wir kennen die ersten 128 \mbox{Bytes} einer Message (inklusive Länge). Wir
können dann aus den ersten beiden Blöcken des verschlüsselten Texts
sowohl den Akkumulator als auch den Status berechnen --- einfach,
indem wir die beiden aufeinanderfolgenden Blöcke ver-XODERn, und die
darin enthaltenen Message--Blöcke herausrechnen. Mit diesem vollständig
rekonstruierten internen Status können wir den Rest des Texts entschlüsseln.

Das klappt nicht, wenn wir eine zweite Runde hinzufügen --- denn dann
befindet sich in jedem Block der verschlüsselten Message die XODER--Verknüpfung
von zwei internen Zuständen. Daraus einen einzelnen herauszurechnen
ist schwer möglich.


\section{Anweisungen zum sicheren Gebrauch}

Kryptographie ist nur sicher, wenn sie sorgfältig betrieben wird.
Die folgenden Anweisungen beschreiben, wie man bekannte Probleme umgeht.
\begin{description}
\item [{Signatur}] Signiert wird der Hash--Wert der Datei, indem er mit
dem privaten Schlüssel verschüsselt wird. Es wird empfohlen, einen
zufälligen Startwert im Akkumulator zu verwenden. Dieser zufällige
Startwert stellt sicher, dass ein Angreifer nicht vorab eine Kollision
berechnen kann, und damit später ein anderes Dokument unterschieben
kann. Nachträglich kann ein Angreifer immer noch eine Kollision finden,
aber das ist erheblich schwerer (verdoppelt die Anzahl Bits in der
Stärke des Hashs). Bruce Schneier empfiehlt, als Startwert den öffentlichen
Schlüssel zu verwenden --- das ist aber wenig sinnvoll, da der dem
Angreifer ja bekannt ist.
\item [{Verschlüsselung}] Verschlüsselt man mit einem Schlüssel mehrere
Nachrichtenteile (z.B. Anhänge in einer E--Mail), so ist es ratsam,
den Akkumulator für jeden Teil mit einem anderen Zufallswert vorzubelegen.
Will man eine Replay--Attacke verhindern, kann man auch einen Timestamp
in die Vorbelegung einbeziehen.\end{description}
\begin{thebibliography}{3}
\bibitem[1]{whirlpool}Vincent Rijmen und Paulo S. L. M. Barreto \emph{Whirlpool}
\emph{Page }\texttt{\href{http://www.larc.usp.br/~pbarreto/WhirlpoolPage.html}{http://www.larc.usp.br/$\sim$pbarreto/Whirlpool Page.html}}

\bibitem[2]{sha-1}Meldung auf Heise Security vom 11.6.2009 \emph{\href{http://www.heise.de/security/news/meldung/140259}{Attacke auf SHA-1 weiter vereinfacht}}

\bibitem[3]{source} Der komplette Quellcode, der zu lang zum Abdrucken
ist, ist Teil des bigForth--Repositorys \texttt{\href{http://www.forth-ev.de/repos/bigforth/wurstkessel.fs}{http://www.forth-ev.de/repos/bigforth/wurst kessel.fs}}

\end{thebibliography}
\end{multicols}
\end{document}
