Mitmachen:¶

  • http://bruckbu.de:10001/
  • http://bruckbu.de:10002/
  • http://bruckbu.de:10003/
  • http://bruckbu.de:10004/
  • ...
  • bis 10012 - wenn wir mehr sind, gebt mir zwei minuten

Mit Passwort MussNichtGanzSoLangeSicherSein...

Dieses Dokument ist ein Jupyter Notebook, d.h. eine interaktive Python-Umgebung.

Beispiel: Schere-Stein-Papier im Chat¶

Problem:¶

  • Alice: "Stein"
  • Bob: "Papier"

Lösungsansätze¶

  • Trusted third party
  • Commitment:
    • Alice entscheidet sich für einee Aktion und sendet ein "Commitment", womit Bob zunächst nichts anfangen kann
    • Bob: sendet Aktion (z.B. "Papier")
    • Alice: sendet Aktion
    • Bob: verifiziert, ob Alices Aktion und Commitment zusammen passen

Hashfunktionen¶

Die Anforderungen an eine kryptographische Hashfunktion sind

  • Kompression: Beliebig lange Eingabe -> Ausgabe fester Länge, $h: \{0,1\}^* \to \{0,1\}^n$
  • Einweg-Funktion: praktisch unmöglich, zu einer Ausgabe eine passende Eingabe zu finden.
  • Kollisionsresistenz: praktisch unmöglich, zwei Eingaben mit der selben Ausgabe zu finden

Weitere wünschenswerte Effekte:

  • nearby collision resistence: es ist schwierig, ähnliche Ausgaben zu finden
  • avalance effect: ähnliche Eingaben haben Ausgaben, die scheinbar nichts miteinander zu tun haben

Sichere Hashfunktionen in der Praxis: SHA2 (sha256, sha384, sha512), SHA3
Unsichere Hashfunktionen in der Praxis: SHA1, MD5

In [165]:
# Commitment, Erster Ansatz:

from hashlib import sha256

my_action = b"Schere"

commitment = sha256(my_action).hexdigest()

print(commitment)


# Was ist das problem?
e6532e1b964c2ef602ae65967f297f80fe2214ef0fbc16e6928ebca0ce741a95
In [169]:
# Nicht deterministisch:

from hashlib import sha256

my_action = b"Stein - TODO: gib hier deine unvorhersehbare Daten ein!"

commitment = sha256(my_action).hexdigest()

print(commitment)
e7f1b9a9e6c94f09311bc75c4b79fe58419044d3822ee538049c1414203f6989
In [ ]:
# Nicht deterministisch, automatisch:

from hashlib import sha256
from random import SystemRandom

rng = SystemRandom()

my_action = b"Schere"
message = my_action + b" - " + rng.randbytes(16)


commitment = sha256(message).hexdigest()


print(commitment)
print(message)
In [ ]:
# Bob: TODO: ausfüllen
# message = ...
# commitment = "..."

print(sha256(message).hexdigest() == commitment)

Weitere Anwendungen von Hashes¶

  • Prüfsumme gegen Manipulation oder übertragungsfehler. Hier muss aber der Hash sicher gespeichert / übertragen werden.
    Reduktion: Hashwert manipulationssicher übertragen -> viele Daten manipulationsicher übertragen
    -> Nützlich als Baustein in weiteren Anwendungen
  • Message Authentication Codes:
    Alice und Bob haben gemeinsames geheimnis k
    Alice schickt Nachricht + sha3(k + Nachricht)
    Achtung: bei sha2 HMAC verwenden.

    Beispiel: Zwei Faktor Authentifizierung wie Google Authenticator oder TANs

  • Speichern von Passwörtern (Historisch?)
    hash(passwort) ist problematisch, als Alternative -> z.B. "Argon2"

Intermezzo: Bits, Bytes, Text, Zahlen, ...¶

  • bit: "binary digit" eine Ziffer 0 oder 1

  • Byte: 8 bit, d.h. Zahlen 0...255, häufig hexadezimal dargestellt: 2A = 2 * 16 + 10 = 42

  • Darstellung von Text als Bytes: viele möglichkeiten, z.B. UTF8:
    Jedes Zeichen hat eine Nummer (-> Unicode, -> ASCII). Die Zeichennummern haben Darstellungen als ein oder mehrere Bytes. Darstellungen von mehreren Zeichen werden aneinander gehängt.

    Hallöchen -> [72, 97, 108, 108, 246, 99, 104, 101, 110] -> [72, 97, 108, 108, 195, 182, 99, 104, 101, 110]
    Hex: 48616c6cc3b66368656e

In [ ]:
"Hallöchen".encode(),    "Hallöchen".encode().hex()
  • Darstellung von Zahlen als Bytes: Bytes als Ziffern in Basis 256.
    • "big endian": erstes byte hat höchsten Stellenwert
    • "little endian": erstes byte hat niedrigsten Stellenwert
In [ ]:
n = 1027 #  = 4 * 256 + 3
print(n.to_bytes(2, "big").hex())
print(n.to_bytes(2, "little"))

Symmetrische Verschlüsselung¶

Verschlüsseln(Klartext, Schlüssel) -> Ciphertext
Entschlüsseln(Ciphertext, Schlüssel) -> Klartext

  • Ohne Schlüssel soll der Ciphertext von zufälligen Daten ununterscheidbar sein

  • Wenn man den Schlüssel mehrfach verwenden will:
    Die Kenntnis von Klartext und Ciphertext soll nichts preisgeben

One time pad¶

  • Nachricht m z.B. in bits darstellen, (m[0], m[1], ...),
  • Schlüssel k mindestens so lang wie die Nachricht, zufällig
  • Ciphertext c definiert als c[i] = m[i] + k[i] (Stellenweise Addieren ohne Übertrag)
Nachricht Hallo  -> 48616c6c6f   <-> 01001000 01100001 01101100 01101100011 01111
Schlüssel Perdox -> 506572646f78 <-> 01010000 01100101 01110010 01100100011 0111101111000
Ciphertext                           00011000 00000100 00011110 00001000000 00000
                                        1   8    0   4    1   E    0   8    0   0

Nachricht Hallo  -> 48616c6c6f   <-> 01001000 01100001 01101100 01101100 01101111
Zufälliger Schlüssel                 11100000 11010010 10100101 01100000 01110010
                                     10101000 10110011 11001001 00001100 00011101
                                        A   8    B   3    C   9    0   C    1   D

Sicher, wenn man jeden Schlüssel nur ein einziges mal verwendet.

In [173]:
# One-Time-Pad in  python
key_bytes = b"Perdox"

# Aufgabe: Nutze zufällige bytes stattdessen - siehe irgendwo oben :)

# key_bytes = bytes.fromhex("....") 

print(key_bytes)
print(key_bytes.hex())
b'Perdox'
506572646f78
In [175]:
msg = "Hallo"
msg_bytes = msg.encode()   # Macht string "Hallo" zu bytes.
In [189]:
n = len(msg_bytes)

ciphertext = bytearray(n)  # Für Ergebnis: so viele bytes wie msg_bytes lang ist anlegen

for i in range(n):
    # Wir arbeiten hier mit bytes statt mit bits, d.h.
    # irgendwie müssen wir 8 bit-additionen auf einmal durchführen.
    # das macht der ^ operator.
    ciphertext[i] = msg_bytes[i] ^ key_bytes[i]


print(ciphertext)
print(ciphertext.hex())

# Entschlüsseln geht genauso wie verschlüsseln - probiert es aus!
bytearray(b'FLAG{oh no, you stole my deepest secret!}')
464c41477b6f68206e6f2c20796f752073746f6c65206d79206465657065737420736563726574217d
In [176]:
msg_bytes = bytearray(b'\x18\x04\x1e\x08\x00')
In [188]:
# Known plaintext attack gegen OTP

# ciphertext_1 und ciphertext_2 wurden mit dem selben Schlüssel erzeugt.

plaintext_1 = b"As OTP is proven to be secure, I will always use it!"
ciphertext_1 = bytes.fromhex("e0c141a1276dee44b7b0ee8147e3238011526aaa56cf971518e22401bef70ab0620dfd3958333916931b261d42bb1018dc3ce002")

ciphertext_2 = bytes.fromhex("e7fe20a90852a60daaffb2d351fa33ce42526ae6518ada1f5de53416abbe598d6209f13646762c5b99")

key_bytes = b'\xa1\xb2a\xees=\xce-\xc4\x90\x9e\xf3(\x95F\xee1&\x05\x8a4\xaa\xb7f}\x81Qs\xdb\xdb*\xf9Bz\x94U4\x13Xz\xe4z_nb\xcec}\xfcU\x94#'
msg_bytes = ciphertext_2

Stromchiffren (Stream Ciphers)¶

  • Statt einem Zufälligem One-Time-Pad wird ein Pseudozufälliges verwendet
  • Der Startwert für den Pseudozufallszahlengenerator wird zusammengesetzt aus Schlüssel (geheim, wiederverwendbar) und Nonce ("Number used ONCE")
  • ciphertext = (nonce, Schlüsselstrom xor klartext)

  • Gängige sichere Stromchiffren sind z.B. ChaCha20 und AES im Counter-Mode

Blockchiffre:¶

  • Verschlüsseln eine Eingabe fester länge zu einer Ausgabe gleicher länge. Am weitesten verbreitet ist vermutlich AES.
  • Um mit einer Blockchiffre Daten anderer Länge zu verschlüsseln, gibt es verschiedene sogenannte "Modes of Operation", z.B. CTR (Counter), GCM (Counter + Authentifizierung), CBC (Cipher Block Chaining)

Couter-Mode: Block Cipher -> Stream Cipher¶

Statt Klartext wird ein Zähler verschlüsselt, die Ausgaben sind der Schlüsselstrom

In [ ]:
from Crypto.Cipher import AES

key = b"ist 16bytes lang"  # oder 32 bytes

cipher = AES.new(key, mode=AES.MODE_CTR) # 
nonce = cipher.nonce    # zufällig. kann auch mit nonce= beim Erstellen angegeben werden
ciphertext = cipher.encrypt(b"der Klartext kann im Counter-Mode jede Laenge haben.")


print(nonce.hex(), ciphertext.hex())
In [ ]:
cipher = AES.new(key, mode=AES.MODE_CTR, nonce=nonce)
pt = cipher.decrypt(ciphertext)
print(pt)

Diffie-Hellman-Schüsselaustausch¶

Bild aus Wikipedia

Modulare Arithmetik (in Python):¶

Wenn man statt normal zu rechnen, immer nur die Reste nach der division durch $n$ betrachtet, funktionieren die meisten bekannten Rechengesetze weiter:

In [163]:
(3 + 10 + 15) % 11 == (2 + 15) % 11 == (2 + 4) % 11 == 6
Out[163]:
True
In [ ]:
(9 * 11) % 11
In [164]:
# Potenzen von 2, mod 11:
for i in range(13):
    print(i, 2**i, 2**i % 11, pow(2, i, 11))
0 1 1 1
1 2 2 2
2 4 4 4
3 8 8 8
4 16 5 5
5 32 10 10
6 64 9 9
7 128 7 7
8 256 3 3
9 512 6 6
10 1024 1 1
11 2048 2 2
12 4096 4 4
  • Potenzen mod $p$ (prim) wiederholen sich nach $p-1$ Schritten
  • Beim Berechnen von $a^b \ \%\ n$ kann man Platz und Zeit sparen, indem man
    • schon Zwischenschritte mod $n$ reduziert
    • $b$ im Zweiersystem darstellt und quadrate nutzt: $5^9 = 5^{2 \cdot 2 \cdot 2 + 1} = 5^{2\cdot 2 \cdot 2} \cdot 5 $
    • Das macht in Python pow(a,b,n)
In [ ]:
# DH in Python

from random import SystemRandom

p = 26620000968396558157058486122762022047658163186550617910789713670105584707837203686745958338704438582689048773144369335500395009787020830018463829975518131094597143856888552926514964146211312961812930093381831783656958664347685794138490752357760389677704342344983908376788668312350379037227937065593445519887213372732655128534070179543933664406307677407706816421792940468094821877210944618013484146000483753306052420260783034722557870719509408486061019840009710023976464989087815331876396002336659605610248016717945748351067194176593112926527800868468153526684544644879979343120714474757093198048023322841938394145463
# p ist eine "safe prime, d.h. (p-1)/2 ist auch prim", generiert mit `openssl dhparam 2048`
# Alternativ: p = 11

g = 2

rng = SystemRandom() # Kryptographischer Zufallszahlengenerator des Betriebssystems

my_secret = rng.randrange(p-1)


# TODO
my_public = 1234

print("SECRET:", my_secret)
print()
print("public", my_public)

# Verteile "public" über den chat
In [ ]:
# Hier den empfangenen Wert einfügen
their_public = 1234

shared = pow(their_public, my_secret, p) # shared == g ** (my_secret * their_secret) % p

# Key derivation: hier gibt es für solche anwendungen optimierte Funktionen, wir nehmen
# jetzt einfach sha256.

shared_bytes = shared.to_bytes(2048//8, "big")
from hashlib import sha256
key = sha256(shared_bytes).digest()

# Jetzt habt ihr in "key" einen gemeinsamen schlüssel. Mit dem AES-Beispiel von oben könnt ihr jetzt nachrichten austauschen.

Diffie-Hellman-Schüsselaustausch¶

  • Aus $A = g^a \ \ \% \ p $ wieder $a$ zu berechnen, nennt man Diskreten Logarithmus. Dafür ist kein allgemeiner schneller Algorithmus bekannt.
  • wir können jetzt über einen öffentlichen Kanal geheime Nachrichten austauschen
  • Jetzt müsste man nur noch wissen, dass auf der anderen Seite die richtige Person / Maschine antwortet...

    • Man kann öffentlichen / Privaten Schlüsselanteil aus Diffie-Hellman fest einem Benutzer zuordnen
    • Das macht z.B. der Messenger "Threema".
    • Nachteil: Schlüssel für ein Benutzerpaar immer gleich, wenn ma den hat, kann man den gesamten Nachrichtenverlauf entschlüsseln
  • Wenn die Geheimnisse für den Diffie-Hellman-Schlüsselaustausch immer frisch generiert werden ("Ephemeral Diffie Hellman"), brauchen wir eine andere Möglichkeit, einem Benutzer eine beweisbare Identität zuzuordnen...

Digitale Signaturen¶

Beispiel: El-Gamal Signatur¶

(nicht verbreitet, weil mit (EC)DSA eine effizientere Alternative existiert)

  • Parameter $p$ und $g$
  • Geheimer schlüssel $a$, öffentlicher Schlüssel $A = g^a \ \ \% \ p$ wie bei Diffie-Hellman
  • Signieren einer Nachricht m:

    • h = hash(m), interpretiert als Zahl
    • $k$: Zufällig, $0 < k < p-1$, $\text{ggT}(k,p-1) = 1$
    • $r = g^k \mod p$
    • $s := k^{-1} \cdot (h - a \cdot r) \ \mod p-1$
    • Signatur besteht aus $r$ und $s$

    • Verifizieren:

      • h = hash(m), interpretiert als Zahl
      • Prüfe $0<r<p$ und $0<s<p-1$
      • Prüfe $g^h = A^r \cdot r^s \mod p$

$A^r \cdot r^s = g^{a \cdot r + k k^{-1} (h - a\cdot r)} = g^h$

PKI (Public Key Infrastructure) und Zertifikate¶

Wie kann ich mit einer kleinen Webseite wie https://perdox-ev.de/ kommunizieren, ohne dass ich deren Publickey kenne?

Webseite sendet eine Zertifikatskette:

  • ein Zertifikat enthält
    • Identität (Webseite, Emailadresse, Firmenname)
    • öffentlichem Schlüssel
    • Gültigkeitszeitraum
    • Hinweis auf den Aussteller ("Issuer") - wieder ein Zertifikat
    • Das ganze ist signiert vom Aussteller
  • Die letzte Stufe muss dem Browser bekannt sein.
    • Ja, das hat fast alle Probleme, die ihr euch denken könnt :)
  • Ganz verschiedene Verschlüsselungs-/Schlüsselaustausch- und Signaturalgorithmen möglich - manche Webseiten haben Forward Secrecy, manche nicht.

S/MIME: Das gleiche für Email - da nicht Interaktiv, keine Forward Secrecy

Verschiedenes:¶

  • PGP: Basiert darauf, dass man Schlüssel mit seinen Kontaktpersonen austauscht, und ein "Web of trust" baut
    • Eingesetzt z.B. für Email, XMPP, Signatur von Softwarepaketen auf Linux-Distributionen
    • keine Forward secrecy
  • Signal Protokoll (Signal, WhatsApp, Matrix, XMPP mit OMEMO)
    • Forward Secrecy!
    • (Legt einen Vorrat von EDH Public Keyshares auf dem Server ab, damit man nicht immer für einen Diffie-Hellman-Schlüsselaustausch online sein muss)

Wenn Zeit ist

  • Weitere Fragen/Themen
  • Google Authenticator implementieren
  • Signal protokoll anschauen