Aus Linux-Magazin 06/2011

Kernel- und Treiberprogrammierung mit dem Kernel 2.6 – Folge 57

Texte mit AES verschlüsseln, Daten komprimieren oder Hashsummen mit MD5 berechnen: Mit dem Crypto-API des Linux-Kernels und etwas Know-how kein Problem!

Die umfassende Internetnutzung und das Bedürfnis nach Sicherheit sorgen für stärkere Verbreitung der Kryptographie. Hardwarehersteller bauen aus diesem Grund seit einigen Jahren entsprechende Erweiterungen in die hauseigenen Chips ein, beispielsweise VIA mit Padlock [1] oder Intel mit dem AES-NI-Befehlssatz [2]. Linux wiederum stellt dem Programmierer mit dem Crypto-API Kryptographie-Routinen zur Verfügung, die entweder rein in Software oder aber Hardware-unterstützt ablaufen. Ziel der Kernelentwickler ist es, dies für den Entwickler transparent zu halten.

Das Crypto-API ist nicht nur für Verschlüsselung zuständig, sondern auch für das Bilden von Hash- und Checksummen, für Kompression oder die Generierung von Zufallszahlen. Durch den modularen Aufbau des Crypto-Layers lassen sich auf der unteren Seite die unterschiedlichsten Algorithmen andocken (siehe Tabelle 1), die sich auf der oberen Seite von verschiedenen Subsystemen wie beispielsweise der Festplattenverschlüsselung (DM-Crypt) oder der geschützten VPN-Kommunikation (IPsec) nutzen lassen (siehe Abbildung 1).

Tabelle 1

Unterstützte Krypto-Algorithmen

Funktion

Algorithmen

Hashsummen

md4, md5, michael.mic, ripemd-128, ripemd-160, ripemd-256, ripemd-320, sha1, sha256, sha512, tiger, vmac, whirlpool

Cipher

3des, aead, aes, anubis, arc4, blowfish, camellia, cast5, cast6, des, fcrypt, khazad, salsa20, seed, serpent, tea,xtea,xeta, twofish

Kompression

deflate, lzo, zlib

Sonstiges

ansi_cprng (Pseudo Random Nummer Generator), kernel random number generator, random number generator, crc32c (Checksummen)

Abbildung 1: Der Crypto-Layer des Linux-Kernels abstrahiert Verschlüsselungsalgorithmen.

Abbildung 1: Der Crypto-Layer des Linux-Kernels abstrahiert Verschlüsselungsalgorithmen.

Transform-Objekte

Basis des Crypto-API im Kernel sind die Transform-Objekte. Für die Bildung von Hashsummen sowie Kompressions- und Verschlüsselungsverfahren gibt es jeweils eigene Objekte. Wenn es um Verschlüsselung geht, hat der Entwickler sogar die Auswahl zwischen vier unterschiedlichen Transforms: Cipher, Blockcipher, Asynchronous Blockcipher und AEAD (Authenticated Encryption with Associated Data). Diese unterscheiden sich bezüglich der Angabe von Quell- und Zielspeicherbereichen und der Funktionalität.

Verstreute Daten

Block-orientierte Transform-Objekte, wozu neben Blockcipher, Asynchronous Blockcipher und AEAD auch Hashsummen gehören, spezifizieren Quelle und Ziel der Operationen auf Basis von Scatter-Gather-Listen (siehe Kasten “Scatter-Gather-Listen”). Stream-orientierte Transforms (Cipher, Compress) verwenden direkt virtuelle Adressen.

Scatter-Gather-Listen

Scatter-Gather-Listen tauchen im Kernel immer dann auf, wenn größere Mengen an Daten zu transferieren sind. Größere Datenmengen – also typischerweise mehr Daten, als in eine so genannte Page (4096 Byte) passen – verteilen sich nämlich im Hauptspeicher häufig über verschiedene physische Adressen.

Scatter-Gather-Listen fassen diese physischen Adressen in einer Datenstruktur zusammen, sodass die Transferfunktionen – etwa das Schreiben der Daten auf die Festplatte – in einem Zug alle notwendigen Informationen besitzen und den Transfer auch per DMA durchführen können. Eigentlich wäre Scatter-Gather-Feld die korrekte Bezeichnung, denn die grundlegende Datenstruktur ist ein Array vom Typ »struct scatterlist«. Jedes Element eines solchen Felds repräsentiert ein zusammenhängendes Fragment im Speicher, das durch eine Page-Adresse, den Startoffset innerhalb der Speicherseite sowie die Länge der dort abgelegten Daten (in Bytes) gekennzeichnet ist.

Ein derartiges Feld kann der Programmierer entweder dynamisch während der Laufzeit allozieren und initialisieren oder dies statisch durch den Compiler erledigen lassen. Im letzteren Fall muss der Code zur Initialisierung »sg_init_table()« aufrufen. Um die einzelnen Feldelemente zu belegen, kommt entweder »sg_set_buf()« oder »sg_set_page()« zum Aufruf – je nachdem, ob die Adresse der Daten als virtuelle oder als Page-Adresse vorliegt. Wurde das Scatter-Gather-Feld per »sg_alloc_table()« angelegt, gibt es »sg_free_table()« nach Gebrauch wieder frei.

Neben den in Tabelle 2 aufgeführten Funktionen gibt es noch eine Reihe weiterer, die insbesondere die Abarbeitung einer Scatter-Gather-Liste vereinfachen.

Tabelle 2

Funktionen zum Aufbau von Scatter/Gather-Listen

Funktionsprototyp

Kurzbeschreibung

int sg_alloc_table(struct sg_table *table, unsigned int nents, gfp_t gfp_mask);

Reserviert und initialisiert eine Scatter/Gather Liste mit »nents« Feldern. »gfp_mask« gibt den Kontext an, in dem die Funktion aufgerufen wird (User-, Kernel- oder Interrupt-Kontext), »table« enthält nach dem Aufruf die Adresse der Liste.

void sg_init_table(struct scatterlist *sg, unsigned int nents);

Initialisiert die Liste »sg« , die »nents« Einträge hat.

void sg_init_one(struct scatterlist *sg, const void *buf, unsigned int length);

Initialisiert die Liste »sg« (die aus einem einzigen Listenelement besteht) und belegt sie mit der Adresse des Speicherbereiches »buf« und der Länge »length« vor.

static inline void sg_set_page(struct scatterlist *sg, struct page *page, unsigned int len, unsigned int offset);

Initialisiert das Listenelement »sg« mit der Seitenadresses »page« , der Länge »len« und dem Offset »offset« innerhalb der Seite.

static inline void sg_set_buf(struct scatterlist *sg, const void *buf, unsigned int buflen);

Initialisiert das Listenelement »sg« mit der virtuellen Adresse »buf« und der Länge »buflen« .

static inline void *sg_virt(struct scatterlist *sg);

Gibt die virtuelle Adresse des Listenelementes »sg« zurück.

Generell arbeiten die Algorithmen synchron, mit dem »ablkcipher«-Transform ist auch eine asynchrone Ver- und Entschlüsselung möglich: Der Crypto-Layer übernimmt Aufträge und übergibt das Ergebnis später per Callback-Funktion dem Auftraggeber. Das AEAD-Transform schließlich kombiniert Verschlüsselung und Hashing, wie es bei IPsec benötigt wird. Vor der Nutzung einer Kryptofunktion im Kernel steht das Anlegen eines geeigneten Transform-Objekts. Dabei ist gleichzeitig der gewünschte Algorithmus anzugeben, beispielsweise »md5«. Vereinfacht ausgedrückt ist hiernach die gewünschte Kryptofunktion mit Übergabe der Quell- und Zielspeicheradressen aufzurufen.

Dass sich der Programmierer im Detail jedoch noch mit Scatter-Gather-Listen, einem Descriptor-Objekt und so etwas wie »digestsize« herumschlagen muss, verdeutlicht dieser Artikel an den Beispielen MD5-Hashsummenbildung und AES-Verschlüsselung.

Hash berechnen

Mit »digestsize« dimensioniert der Entwickler die Größe des Speicherbereichs, der das Ergebnis der Hashsummenbildung aufnimmt, den Hashwert. Dieser ist – abhängig vom gewählten Hashalgorithmus – unterschiedlich groß und lässt sich über die Inline-Funktion »crypto_hash_digestsize()« abfragen. Das erwähnte Descriptor-Objekt dient als Container für die verschiedenen Kryptofunktionen. Der Programmierer initialisiert es mit der Adresse des Transform-Objekts.

Die Scatter-Gather-Listen schließlich enthalten die Adressen der Speicherbereiche. Sie bilden den aus Sicht einer Applikation zusammenhängenden Speicherbereich auf die physischen, realen Speicherorte ab, also die meist verstreut liegenden zugehörigen Pages.

Listing 1 zeigt die Bildung der MD5-Summe des kurzen Datensatzes “Linux- Magazin” im Kernel. Zeile 27 reserviert das Transform-Objekt unter Angabe des gewünschten Hashsummen-Algorithmus (»md5«), Zeile 33 initialisiert das Descriptor-Objekt mit der Adresse des Transform. Die Vorbereitung der Scatter-Gather-Listen ist in den Zeilen 24 und 25 zu sehen, und schließlich wird in Zeile 36 der Hashwert gebildet.

Listing 1

MD5-Summe des Textes “Linux-Magazin” (»lm.c«)

01 #include <linux/crypto.h>
02 #include <linux/err.h>
03 #include <linux/scatterlist.h>
04
05 static struct scatterlist sg_text;
06 static char result[128];
07 static struct crypto_hash *tfm;
08 static struct hash_desc desc;
09
10 static int __init mod_init( void )
11 {
12         struct page *pg;
13         char *buf;
14         int i;
15
16         pg = alloc_pages( GFP_USER, 0 );
17         if (pg==NULL) {
18                 printk("alloc_pages failed\n");
19                 return -EIO;
20         }
21         buf = page_address( pg );
22         memcpy( buf, "Linux-Magazin", 13 );
23
24         sg_init_table( &sg_text, 1 );
25         sg_set_page( &sg_text, pg, 13, 0 );
26
27         tfm = crypto_alloc_hash("md5", 0, CRYPTO_ALG_ASYNC);
28         if (IS_ERR(tfm)) {
29                 printk("crypto_alloc_hash failed\n");
30                 goto out_free_pages;
31         }
32
33         desc.tfm = tfm;
34         desc.flags = 0;
35
36         if (crypto_hash_digest(&desc, &sg_text, 13, result)) {
37                 printk("crypto_hash_digest failed\n");
38         } else {
39                 printk("Ergebnis md5sum:\n");
40                 for (i=0; i <crypto_hash_digestsize(tfm); i++ ) {
41                         printk("%02x", (unsigned char) result[i]);
42                 }
43                 printk("\n");
44         }
45
46         crypto_free_hash(tfm);
47 out_free_pages:
48         free_pages( (unsigned long) page_address(pg), 0 );
49         return -EIO;
50 }
51
52 static void __exit mod_exit( void )
53 {
54         return;
55 }
56
57 module_init( mod_init );
58 module_exit( mod_exit );
59 MODULE_LICENSE("GPL");

Dass dieser korrekt ist, lässt sich leicht – wie in Abbildung 2 dargestellt – überprüfen, indem man den Hashwert mit dem »md5sum«-Kommando erzeugt und mit der Ausgabe des Kernels im Syslog vergleicht. Der negative Rückgabewert (Zeile 49) signalisiert dem Kernel übrigens, dass er das Modul direkt wieder entladen soll. Das erspart beim Testen das ansonsten notwendige manuelle Entladen.

Abbildung 2: Die Gegenprobe mit »md5sum« beweist: Der Code aus

Abbildung 2: Die Gegenprobe mit »md5sum« beweist: Der Code aus

Verschlüsseln

Daneben bietet das Crypto-API des Kernels auch Verfahren zur Verschlüsselung an. Hier ist ein kleiner Ausflug in die Kryptographie [3] angebracht: Bei der Verschlüsselung wird eine Klartextnachricht mit Hilfe von Verschlüsselungsalgorithmen – so genannte Cipher – und unter Zuhilfenahme eines geheimen Schlüssels in den Ciphertext gewandelt. Unter der Voraussetzung, dass sowohl Algorithmus als auch der eingesetzte Schlüssel als sicher gelten, kann nur derjenige den Ciphertext zurückwandeln (entschlüsseln) und interpretieren, der im Besitz des passenden Schlüssels ist.

Prinzipiell sind zwei Arten der Verschlüsselung zu unterscheiden: die symmetrische Verschlüsselung von der asymmetrischen. Die asymmetrische (Public-Key-Verfahren) setzt zur Ver- und Entschlüsselung unterschiedliche Schlüssel ein. Vorteil: Eine Verschlüsselung ist gezielt für einen dedizierten Empfänger beziehungsweise Empfängerkreis möglich. Die symmetrische Verschlüsselung demgegenüber setzt für beide Vorgänge denselben Key ein.

Die sichere Übergabe des geheimen Schlüssels und der Umstand, dass mehrere Personen oder Systeme den (geheimen) Schlüssel kennen, ist aus Sicht der Kryptographen bedenklich und erfordert besondere Sicherungsmaßnahmen. Andererseits ermöglichen symmetrische Verfahren aber eine sehr schnelle Kodierung und Dekodierung.

Die Cipher selbst lassen sich nach ihrer internen Arbeitsweise unterscheiden: Streamcipher verschlüsseln Nachrichten beliebiger Länge und typischerweise Zeichen für Zeichen, während Blockcipher Nachrichten blockweise verarbeiten. Nachrichten, die nicht dem Vielfachen einer Blöckgröße entsprechen, werden mit Nullen aufgefüllt.

Sicher oder nicht sicher

Falls der Algorithmus jeden Block unabhängig bearbeitet, spricht man vom Electronic Code Book Mode (EBC). Da dieses Verfahren für mehrfach vorkommende identische Blöcke den gleichen Ciphertext erstellt, gilt das Verfahren nur als eingeschränkt sicher (siehe Abbildung 3a). Das Cipher Block Chaining (CBC) vermengt den jeweils vorangehenden Block mit dem zu verschlüsselnden, zum Beispiel per XOR, wobei ein als Initialisierungsvektor (IV) bezeichneter Block für die Verknüpfung mit dem allerersten Block zuständig ist (Abbildung 3b).

Abbildung 3c: Das Beste zweier Welten: Der Counter Mode (CTR) schafft es, durch die Verknüpfung jedes Blocks mit einem eigenen IV zugleich sicher und parallelisierbar zu sein.

Abbildung 3c: Das Beste zweier Welten: Der Counter Mode (CTR) schafft es, durch die Verknüpfung jedes Blocks mit einem eigenen IV zugleich sicher und parallelisierbar zu sein.

Abbildung 3b: Verkettung: Cipher Block Chaining (CBC) ist sicher, aber nicht für Multicore-Computer geeignet.

Abbildung 3b: Verkettung: Cipher Block Chaining (CBC) ist sicher, aber nicht für Multicore-Computer geeignet.

Abbildung 3a: Das Verfahren Electronic Code Book Mode (EBC) gilt als nur beschränkt sicher.

Abbildung 3a: Das Verfahren Electronic Code Book Mode (EBC) gilt als nur beschränkt sicher.

Der IV wird typischerweise mit Zufallszahlen vorbelegt und braucht nicht geheim gehalten zu werden. CBC hat aber gerade in Zeiten von Multicore-Systemen einen entscheidenden Nachteil: Die Verschlüsselung ist nur sequenziell, also Block für Block möglich, nicht parallel.

CTR macht Tempo

Diesen Nachteil überwindet der Counter Mode (CTR): Er verschlüsselt jeden Block mit einem Initialisierungsvektor (Abbildung 3c). Der Inhalt des für jeden Block unterschiedlichen IV ergibt sich aus einem einmal festgelegten (Zufalls-)Wert und der Blocknummer durch sinnvolle mathematische Verknüpfung (zum Beispiel XOR). Das Verschlüsseln von Daten mit dem Linux-Kernel verwendet eines der Cipher-Transforms aus dem Crypto-API. Für die weitverbreitete AES-Verschlüsselung eignet sich beispielsweise das Blockcipher-Transform. Die Verschlüsselung transformiert eine Klartextnachricht mit Hilfe des Schlüssels und mit einem Initialisierungsvektor (bei CBC und CTR) in den Ciphertext.

Erforderliche Parameter

Den Speicherbereich, der den Schlüssel enthält, übergibt der Programmierer zusammen mit der zugehörigen Schlüssellänge der Funktion »crypto_blkcipher_setkey()«. Den Speicherbereich für den Initialisierungsvektor stellt das Blockcipher-Transform-Objekt selbst zur Verfügung (siehe Abbildung 4). Er muss damit nur noch belegt werden.

Abbildung 4: Zur Verschlüsselung notwendige Datenstrukturen: der Name des Transform-Objekts, der Schlüssel, die Speicherorte von Klar- und Ciphertext als Scatter-Gather-Listen (»sg«) sowie die Länge.

Abbildung 4: Zur Verschlüsselung notwendige Datenstrukturen: der Name des Transform-Objekts, der Schlüssel, die Speicherorte von Klar- und Ciphertext als Scatter-Gather-Listen (»sg«) sowie die Länge.

In Form einer Scatter-Gather-List werden noch zwei weitere Speicherbereiche benötigt: Der erste enthält die Klartextnachricht, der zweite nimmt die verschlüsselte Nachricht auf, den Ciphertext. Mit Klartextnachricht, Key, Initialisierungsvektor und Speicher für das Ergebnis startet durch Aufruf der Funktion »crypto_blkcipher_encrypt()« die Verschlüsselung. Am Ende ist der Ergebnispuffer mit dem Ciphertext gefüllt. Das Entschlüsseln funktioniert genauso: Der Programmierer muss Ciphertext, Key, IV und Speicher für das Ergebnis – in diesem Fall die Klartextnachricht – bereitstellen.

Listing 2 zeigt die Ver- und Entschlüsselung des Textes “Linux Magazin” mit AES im Blockchaining-Modus (»cbc«). Der Einfachheit halber kommt als Key und ebenso als IV eine Folge von Nullen zum Einsatz. AES ist ein Blockcipher, verschlüsselt also immer nur Daten, die ein Vielfaches eines Blocks (256 Bit) lang sind. Ist die Nachricht etwas kürzer, füllt man sie einfach mit Nullen auf.

Listing 2

Verschlüsseln und Entschlüsseln im Kernel (»aes_user.c«)

01 #include <linux/crypto.h>
02 #include <linux/scatterlist.h>
03
04 #define DATA_SIZE  32
05
06 static int __init aes_user_init(void)
07 {
08         char key[16], *iv;
09         struct crypto_blkcipher *tfm_cbc;
10         struct scatterlist sg[3];
11         int ret;
12         char *msg, *encrypted, *decrypted;
13         struct blkcipher_desc desc;
14         size_t ivsize;
15
16         printk("aes-user\n");
17         memset(key, 0, sizeof(key));
18
19         tfm_cbc = crypto_alloc_blkcipher("cbc(aes)",0,CRYPTO_ALG_ASYNC);
20         if ( IS_ERR(tfm_cbc) )
21                 return -EIO;
22
23         desc.tfm = tfm_cbc;
24         desc.flags = 0;
25
26         ret = crypto_blkcipher_setkey(tfm_cbc, key, sizeof(key));
27         if ( ret<0 )
28                 goto out;
29
30         msg = kzalloc(DATA_SIZE, GFP_KERNEL);
31         if ( !msg )
32                 goto out;
33
34         encrypted = kzalloc(DATA_SIZE, GFP_KERNEL);
35         if ( !encrypted )
36                 goto alloc_encrypted_failed;
37
38         decrypted = kzalloc(DATA_SIZE, GFP_KERNEL);
39         if ( !decrypted )
40                 goto alloc_decrypted_failed;
41
42         memcpy(msg, "Linux Magazin", 13 );
43
44         sg_init_table( sg, ARRAY_SIZE(sg) );
45         sg_set_buf( &sg[0], msg, DATA_SIZE );
46         sg_set_buf( &sg[1], encrypted, DATA_SIZE );
47         sg_set_buf( &sg[2], decrypted, DATA_SIZE );
48
49         iv = crypto_blkcipher_crt(tfm_cbc)->iv;
50         ivsize = crypto_blkcipher_ivsize(tfm_cbc);
51         memset(iv, 0, ivsize);
52
53         ret = crypto_blkcipher_encrypt(&desc, &sg[1], &sg[0], DATA_SIZE);
54         if ( ret<0 )
55                 goto crypt_failed;
56
57         iv = crypto_blkcipher_crt(tfm_cbc)->iv;
58         ivsize = crypto_blkcipher_ivsize(tfm_cbc);
59         memset(iv, 0, ivsize);
60
61         ret = crypto_blkcipher_decrypt(&desc, &sg[2], &sg[1], DATA_SIZE);
62         if ( ret<0 )
63                 goto crypt_failed;
64
65         print_hex_dump(KERN_INFO,"mesg> ",DUMP_PREFIX_NONE,16,1,msg,13,1);
66         print_hex_dump(KERN_INFO,"encr> ",DUMP_PREFIX_NONE,16,1,encrypted,13,1);
67         print_hex_dump(KERN_INFO,"decr> ",DUMP_PREFIX_NONE,16,1,decrypted,13,1);
68
69 crypt_failed:
70         kfree(decrypted);
71 alloc_decrypted_failed:
72         kfree(encrypted);
73 alloc_encrypted_failed:
74         kfree(msg);
75 out:
76         crypto_free_blkcipher(tfm_cbc);
77         return -EIO;
78 }
79
80 static void __exit aes_user_exit(void)
81 {
82 }
83
84 module_init(aes_user_init);
85 module_exit(aes_user_exit);
86 MODULE_LICENSE("GPL");

Der Code zeigt im Syslog (auf vielen Systemen die Datei »/var/log/messages«) die Klartextnachricht (»mesg«), den Ciphertext (»encr«) und die entschlüsselte Nachricht (»decr«) an (siehe Abbildung 5). Es versteht sich von selbst, dass ein Entwickler beim Praxiseinsatz den Key intelligenter wählt, besonders schützt und nicht für jedermann sichtbar im Quellcode hinterlegt.

Abbildung 5: Blick ins Syslog: Die Ausgabe des Moduls »aes_user.ko« – aus dem Code in

Abbildung 5: Blick ins Syslog: Die Ausgabe des Moduls »aes_user.ko« – aus dem Code in

Keine asymmetrische Verschlüsselung

Die übrigen vier, hier nicht weiter vorgestellten Transform-Objekte arbeiten ähnlich. Leider gibt es insgesamt nur sehr wenig Dokumentation. Im Wesentlichen existiert nur die Textdatei in [4]. Der Programmierer ist praktisch gezwungen, sich die zugehörigen Funktionsnamen inklusive der erforderlichen Parameter aus dem Quelltext der Crypto-Headerdatei [5] rauszusuchen.

Hinzu kommt, dass das Crypto-API des Linux-Kernels Gegenstand aktiver Weiterentwicklung ist. Das Ziel von Maintainer Herbert Xu ist es, eine universelle Programmierschnittstelle zu schaffen, die – analog zu »memcpy« – Daten transferiert und dabei falls notwendig auch transformiert. Dank des zurzeit leider sehr spärlich genutzten asynchronen Interface lässt sich dabei auch Parallelverarbeitung auf Multicore-Maschinen nutzen.

Zukunftsvision

Außerdem befindet sich eine Progammierschnittstelle zum Userspace in Entwicklung, sodass auch normale Linux-Programme bequem und transparent Verschlüsselung einsetzen können, und das nach Möglichkeit mit Hardware-Unterstützung. Sobald auch noch die asymmetrischen Verschlüsselungsverfahren hinreichend Umsetzung finden, ist Linux im Bereich Kryptographie bestens ausgestattet.

Infos

  1. Wikipedia, Padlock Security Engine: http://de.wikipedia.org/wiki/PadLock
  2. Wikipedia, AES instruction set: http://en.wikipedia.org/wiki/AES_instruction_set
  3. Wikipedia, Kryptographie: http://de.wikipedia.org/wiki/Kryptografie
  4. Herbert Xu, “Scatterlist Cryptographic API”: http://lxr.free-electrons.com/source/Documentation/crypto/api-intro.txt
  5. Headerdatei »linux/crypto.h«: http://lxr.free-electrons.com/source/include/linux/crypto.h

Der Autor

Eva-Katharina Kunst, Journalistin, und Jürgen Quade, Professor an der Hochschule Niederrhein, sind seit den Anfängen von Linux Fans von Open Source. Mittlerweile ist die dritte Auflage ihres Buches “Linux Treiber entwickeln” erschienen.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben