Aus Linux-Magazin 08/2011

Kernel- und Treiberprogrammierung mit dem Kernel 2.6 – Folge 58

Der Linux-Kernel bietet seine Verschlüsselungsfunktionen auch zum asynchronen Zugriff an. Ineinander geschachtelte Datenstrukturen und undokumentierte Funktionen machen dem Programmierer allerdings das Leben schwer. Diese Kern-Technik schafft Abhilfe.

Wenn die Kernelentwickler von Datentransformation sprechen, meinen sie damit das Ver- und Entschlüsseln, Komprimieren oder das Bilden von Hashsummen. Geeignete Interfaces stellen den Zugriff auf die dafür implementierten Algorithmen sicher, die ihrerseits über definierte Schnittstellen an das Crypto-Subsystem des Linux-Kernels andocken.

Synchron – asynchron

Bisher finden die Transformationen vorwiegend synchron statt, wie es die vorige Ausgabe der Kern-Technik beschrieben hat [1]: Ein User, beispielsweise die Festplattenverschlüsselung, übergibt einen Verschlüsselungsauftrag an das Crypto-Subsystem und wartet bis zum Abschluss der Transformation. Erst dann erteilt er den nächsten Auftrag. Hardware-Verschlüsselung oder Parallelverarbeitung kommen dabei aber nicht zum Zug. Performanter geht es mit dem asynchronen Zugriff, wie ihn auch die Diplomarbeit von Sebastian Siewior [2] vorstellt.

Asynchron bedeutet, dass der User dem Crypto-Subsystem einen oder mehrere Aufträge zum Bearbeiten übergibt und die erfolgreiche oder vergebliche Auftragsbearbeitung zu einem späteren Zeitpunkt prüft (Abbildung 1). Der angeforderte Algorithmus wiederum muss die Aufträge in die Warteschlange einreihen und starten. Die Nachricht über Ge- oder Misslingen teilt das Crypto-Subsystem dem User später mit, indem es eine Callback-Funktion aufruft.

Abbildung 1: Der synchrone Zugriff (oben) auf das Crypto-API des Kernels bringt Wartezeiten mit sich. Bessere Performance verspricht die asynchrone Variante (unten).

Abbildung 1: Der synchrone Zugriff (oben) auf das Crypto-API des Kernels bringt Wartezeiten mit sich. Bessere Performance verspricht die asynchrone Variante (unten).

In der Theorie mag das recht einfach erscheinen, in der Praxis erweisen sich ineinander geschachtelte Datenstrukturen und nicht dokumentierte Funktionen als Stolpersteine. Grundwissen und Quellcodestudium helfen weiter.

Dabei ist für jene, die performant verschlüsseln möchten, der erste Schritt noch einfach: Er spezifiziert den gewünschten Algorithmus und reserviert mit dieser Information das Transform-Objekt vom Typ »ablkcipher_tfm« . Existiert für den Algorithmus keine asynchrone Implementierung, gibt die Reservierungsfunktion einen Wert ungleich 0 zurück. Im anderen Fall wird der zu verwendende Schlüssel an das Transform-Objekt gebunden (Abbildung 2, (1)).

Abbildung 2: Softwarestruktur eines im Kernel asynchron zugreifenden Users.

Abbildung 2: Softwarestruktur eines im Kernel asynchron zugreifenden Users.

Der nachfolgende Schritt bereitet die Transformations-Aufträge und Übergabe an das Crypto-Subsystem vor. Dazu reserviert der User ein Request-Objekt und initialisiert es mit den Adressen der Quell- und Zielspeicherbereiche (als Scatter-Gather-Listen, siehe dazu [1]) und einer Callback-Funktion (Abbildung 2, (2)). Diese spielt eine zentrale Rolle für den dritten Block, der das Durchführen des Auftrags überwacht (3).

Hierbei fällt auf, dass das Crypto-Subsystem einen Auftrag manchmal auch synchron abarbeitet. Ob der Auftrag direkt abgearbeitet (synchron) oder zur Abarbeitung in die Queue gestellt wurde, ist am Rückgabewert der Funktion zur Auftragsvergabe ersichtlich. Im ersten Fall gibt die Funktion 0 zurück, sonst »-EINPROGRESS« oder »-EBUSY« .

Im Fall der asynchronen Abarbeitung ruft das Crypto-Subsystem nach dem Bearbeiten die Callback-Funktion auf und übergibt ihr die Adresse des Auftragsobjekts. Liegen die Ergebnisse sämtlicher Aufträge vor, beginnt im vierten Schritt das Aufräumen: die Freigabe aller Auftragsobjekte, aber auch des Transform-Objekts (Abbildung 2, (4)).

Entschlüsseln

Listing 1 zeigt den Einsatz der asynchronen Schnittstelle. Das Beispiel dekodiert eine Geheimnachricht (Ciphertext), die per XOR-Verfahren mit dem Key »Mein Schluessel.« verschlüsselt wurde. Um den Code möglichst übersichtlich zu halten, beschränkt sich die normalerweise aufwändige Verwaltung der Request-Objekte (»struct ablkcipher_request« ) auf die Reservierung und Freigabe eines einzelnen Objekts. Zum Entschlüsseln einer Nachricht, die kürzer als 16 Byte ist, reicht das aus.

Listing 1

Asynchrone Verschlüsselung (ablk_user.c, Teil 1)

001 #Include <linux/crypto.h>
002 #include "internal.h"
003 #include <linux/scatterlist.h>
004
005 #define CIPHERTEXT "\x01\x0c\x07\x1b\x58\x73\x2e\x09"\
006     "\x0b\x14\x1f\x1a\x1d\x65\x6c\x2e"
007
008 static char *key = "Mein Schluessel.";
009
010 struct crypt_result {
011   struct completion completion;
012   int err;
013 };
014
015 static void request_done(struct crypto_async_request *req, int err)
016 {
017   struct crypt_result *res = req->data;
018
019   printk("request_done()\n");
020   if (err == -EINPROGRESS)
021     return;
022   res->err = err;
023   complete(&res->completion);
024 }
025
026 static int __init ablkuser_init( void )
027 {
028   int ret;
029   struct scatterlist sg[2];
030   char *encrypted, *decrypted;
031   struct crypto_ablkcipher *tfm;
032   struct ablkcipher_request *req;
033   struct crypt_result result;
034
035   printk("ablkuser_init\n");
036   encrypted = kzalloc(16, GFP_KERNEL);
037   if (!encrypted)
038     goto alloc_encrypted_failed;
039   decrypted = kzalloc(16, GFP_KERNEL);
040   if (!decrypted)
041     goto free_encrypted;
042
043   init_completion(&result.completion);
044
045   memcpy(encrypted, CIPHERTEXT, 16);
046
047   sg_init_table( sg, ARRAY_SIZE(sg) );
048   sg_set_buf( &sg[0], encrypted, 16 );
049   sg_set_buf( &sg[1], decrypted, 16 );
050
051   tfm = crypto_alloc_ablkcipher("ablk", 0, 0);
052   if (IS_ERR(tfm)) {
053     printk("crypto_alloc_ablkcipher failed %ld\n", (long)tfm);
054     goto free_decrypted;
055   }
056   ret = crypto_ablkcipher_setkey(tfm, key, strlen(key));
057   if (ret<0) {
058     printk("crypto_ablkcipher_setkey failed\n");
059     goto free_cipher;
060   }
061   /* Auftragsvergabe */
062   req = ablkcipher_request_alloc(tfm, GFP_KERNEL);
063   if (!req) {
064     printk("ablkcipher_request_alloc failed\n");
065     goto free_cipher;
066   }
067   ablkcipher_request_set_callback(req, CRYPTO_TFM_REQ_MAY_BACKLOG,
068       request_done, &result);
069   crypto_ablkcipher_clear_flags(tfm, ~0);
070   ablkcipher_request_set_crypt(req, &sg[0], &sg[1], 16, NULL);
071   ret = crypto_ablkcipher_decrypt(req);
072   if (ret == 0) {
073     request_done( &req->base, 0 );
074   } else if (ret!=-EINPROGRESS && ret!=-EBUSY) {
075     printk("crypte_ablkcipher_decrypt failed %d\n", ret);
076     goto free_all;
077   }
078   ret = wait_for_completion_interruptible(&result.completion);
079   if (ret || (ret = result.err)) {
080     printk("decryption failed %d\n", ret);
081     goto free_all;
082   }
083
084   print_hex_dump(KERN_INFO,"encr> ",DUMP_PREFIX_NONE,16,1,encrypted,16,1);
085   printk("\n");
086   print_hex_dump(KERN_INFO,"decr> ",DUMP_PREFIX_NONE,16,1,decrypted,16,1);
087   printk("\n");
088
089 free_all:
090   ablkcipher_request_free(req);
091 free_cipher:
092   crypto_free_ablkcipher(tfm);
093 free_decrypted:
094   kfree(decrypted);
095 free_encrypted:
096   kfree(encrypted);
097 alloc_encrypted_failed:
098   return -EIO; /* macht das Testen einfacher */
099 }
100
101 static void __exit ablkuser_exit( void ) {
102   return;
103 }
104
105 module_init(ablkuser_init);
106 module_exit(ablkuser_exit);
107
108 MODULE_LICENSE("GPL");

Das Ende der Auftragsbearbeitung lässt sich am besten mit Hilfe des Completion-Objekts verfolgen. Nachdem er die Aufträge dem Crypto-Subsystem zum Bearbeiten übergeben hat, ruft der User für jeden Auftrag einmal die Funktion »wait_for-completion()« auf (Abbildung 1). Die legt die Instanz schlafen, bis das korrespondierende »complete()« erfolgt, bis also die Callback-Funktion zum Ende des Auftrags aktiviert wurde.

Da sein Code neben dem Completion-Objekt typischerweise auch noch den Fehlerstatus eines Auftrags verwalten muss, deklariert der Programmierer für all diese Daten eine eigene Datenstruktur (Listing 1, Zeile 10) und instanziert sie für jeden Auftrag. Der Beispielcode reserviert zur Vereinfachung Speicher für diese Datenstruktur auf dem Stack (Zeile 33).

Callback

Etwas komplexer gerät der Code dadurch, dass ein asynchroner Auftrag auch synchron, also direkt bearbeitet werden kann. In diesem Fall kommt es nie zum Aufruf der Callback-Funktion durch das Crypto-Subsystem; daher wird die Callback-Funktion stellvertretend aktiviert (Listing 1, Zeile 73). Innerhalb der Callback-Funktion selbst werden der Fehlercode gesetzt und das Completion-Objekt aktiviert. Die Callback-Funktion muss im Übrigen so programmiert sein, dass sie sich in jedem Kontext aufrufen lässt. Innerhalb dieser Funktion ist es daher beispielsweise nicht möglich, den gerade aktiven Rechenprozess in den Schlafzustand zu versetzen.

Listing 2 zeigt, wie der Programmierer eigene Algorithmen an das Crypto-Subsystem des Kernels andockt. Im Kern beschreibt es ein Objekt vom Typ »struct crypto_alg« . Dieses Objekt gibt es für jede Transformation (Cipher, Blockcipher, Asynchronous Blockcipher, AEAD) in einer eigenen Ausprägung. Welche vorliegt, spezifiziert ein Flag im Attribut »cra_flags« . Die je nach Ausprägung unterschiedlichen Attribute realisiert der Programmierer über das Konstrukt des Union »cra_u« .

Listing 2

Asynchroner Blockcipher (ablk.c)

01 #include <linux/module.h>
02 #include <linux/interrupt.h>
03 #include <linux/scatterlist.h>
04 #include <crypto/algapi.h>
05
06 static struct tasklet_struct ablk_tasklet;
07
08 struct ablk_ctx {
09   char key[16];
10 };
11
12 static void ablk_doit( unsigned long data )
13 {
14   int error = 0, i;
15   struct ablkcipher_request *req = (struct ablkcipher_request *) data;
16   char *src, *dst;
17   struct ablk_ctx *ctx;
18
19   printk("ablk_doit(%p)\n", req );
20   src = sg_virt( req->src );
21   dst = sg_virt( req->dst );
22   ctx = crypto_tfm_ctx(req->base.tfm);
23   for( i=0; i<req->nbytes; i++ )
24     dst[i] = src[i] ^ ctx->key[i];
25   req->base.complete(&req->base, error);
26 }
27
28 static int ablk_setkey(struct crypto_ablkcipher *tfm,
29     const u8 *key, unsigned int keylen)
30 {
31   struct ablk_ctx *ctx = crypto_tfm_ctx(&tfm->base);
32
33   printk("ablk_setkey( %p, %d )\n", key, keylen );
34   if (keylen == 16) {
35     memcpy(ctx->key, key, keylen);
36     return 0;
37   }
38   return -EINVAL;
39 }
40
41 int ablk_decrypt(struct ablkcipher_request *req )
42 {
43   printk("ablk_decrypt( %p )\n", req);
44   tasklet_init( &ablk_tasklet, ablk_doit, (unsigned long)req );
45   tasklet_schedule( &ablk_tasklet );
46   return -EINPROGRESS;
47 }
48
49 int ablk_encrypt(struct ablkcipher_request *req )
50 {
51   printk("ablk_encrypt( %p )\n", req);
52   tasklet_init( &ablk_tasklet, ablk_doit, (unsigned long)req );
53   tasklet_schedule( &ablk_tasklet );
54   return -EINPROGRESS;
55 }
56
57 static struct crypto_alg ablkcipher_ablk = {
58   .cra_list       = LIST_HEAD_INIT(ablkcipher_ablk.cra_list),
59   .cra_flags      = CRYPTO_ALG_TYPE_ABLKCIPHER|CRYPTO_ALG_ASYNC,
60   .cra_blocksize  = 16,
61   .cra_ctxsize    = sizeof(struct ablk_ctx),
62   .cra_priority   = 100,
63   .cra_name       = "ablk",
64   .cra_type       = &crypto_ablkcipher_type,
65   .cra_u          = { .ablkcipher = {
66     .setkey       = ablk_setkey,
67     .encrypt      = ablk_encrypt,
68     .decrypt      = ablk_decrypt,
69     .geniv        = NULL,
70     .min_keysize  = 0,
71     .max_keysize  = 16,
72     .ivsize       = 0,
73     },
74   }
75 };
76
77 static int __init crypto_ablk_mod_init(void)
78 {
79   printk("crypto_ablk_mod_init()\n");
80   return crypto_register_alg(&ablkcipher_ablk);
81 }
82
83 static void __exit crypto_ablk_mod_exit(void)
84 {
85   printk("crypto_ablk_mod_exit()\n");
86   tasklet_kill( &ablk_tasklet );
87   crypto_unregister_alg(&ablkcipher_ablk);
88 }
89
90 module_init(crypto_ablk_mod_init);
91 module_exit(crypto_ablk_mod_exit);
92 MODULE_LICENSE("GPL");

Obligatorisch ist es, für jeden Algorithmus einen Namen und eine Priorität zu vergeben. Namen dürfen im Übrigen mehrfach vorkommen, wenn sie auch real den gleichen Algorithmus umsetzen. Das Crypto-Subsystem ermöglicht nämlich die Koexistenz verschiedener Implementierungen. Ist ein Algorithmus mehrfach umgesetzt, wählt das Subsystem die Implementierung mit der höchsten Priorität. Normalerweise besitzen Algorithmen die Priorität 100, besonders optimierte Algorithmen – etwa durch eine Assembler-Kodierung – haben die Priorität 200, Hardware-unterstützte Implementierungen 300 oder 400 (Abbildung 3).

Abbildung 3: Das Crypto-API des Linux-Kernels gibt bereitwillig Auskunft über seine Fähigkeiten: Die virtuelle Datei »/proc/crypto« listet auf, welche Transformationen und Attribute das Betriebssystem bereitstellt.

Abbildung 3: Das Crypto-API des Linux-Kernels gibt bereitwillig Auskunft über seine Fähigkeiten: Die virtuelle Datei »/proc/crypto« listet auf, welche Transformationen und Attribute das Betriebssystem bereitstellt.

Objekte und Parameter

Sobald ein User sich beim Crypto-Subsystem unter Angabe des Algorithmen-Namens anmeldet, wird vom Algorithmen-Objekt ein Transform-Objekt abgeleitet. Dieses Objekt enthält nicht nur die Adressen der eigentlichen Transformierungsfunktionen (wie beispielsweise Verschlüsseln), sondern auch die notwendigen instanzenspezifischen Parameter. Möchte ein User beispielsweise Daten ver- oder entschlüsseln, speichert das Objekt den zugehörigen Schlüssel als so genannten Kontext ab.

Weil der Kontext nicht nur algorithmenspezifisch, sondern auch implementierungsspezifisch ist, muss der Programmierer den Speicherbedarf hierfür dem Algorithmenobjekt in dem Attribut »cra_ctxsize« noch mitgeben. Der Speicher für das Transform-Objekt wird dann so groß bemessen, dass sich der Kontext im selben Speicherbereich ablegen lässt. Typischerweise deklariert der Programmierer für den Kontext eine eigene Datenstruktur, wie dies in Listing 2 (Zeile 8) erkennbar ist.

Bitte warten

Im Fall eines asynchronen Blockcipher bekommen die Methoden des Algorithmen-Objekts zum Ver- und zum Entschlüsseln jeweils die Adresse des Request-Objekts übergeben. Hier finden sie insbesondere die Speicheradressen für die Transformation und einen eventuell vorhandenen Initialisierungsvektor. Wird der Auftrag direkt behandelt und erfolgreich abgeschlossen, geben die Methoden 0 zurück, wird der Auftrag asynchron bearbeitet, »-EINPROGRESS« .

Listing 2 empfindet die asynchrone Verarbeitung durch Einsatz eines Tasklet nach. Ein Tasklet ist eine Routine, die das System nach dem nächsten Interrupt abarbeitet und die im Interruptkontext abläuft. Eine produktive Implementierung würde die Funktionalität wohl in einer Interrupt-Service-Routine oder in einem Kernelthread umsetzen. Die Funktion, die das Ende der Bearbeitung identifiziert – im Beispiel das Tasklet –, ruft dann die im Request abgelegte Callback-Funktion (Listing 2, Zeile 25) auf und übergibt ihr den Fehlerstatus, bei erfolgreichem Abschluss des Auftrags also eine 0.

Kompliziert wird die Implementierung eines Transform-Algorithmus durch die komplexen, ineinander geschachtelten Datenstrukturen (Abbildung 4). Diese sind zudem oft unterschiedlich ausgeprägt: Neben globalen Elementen finden sich dort instanzenspezifische Methoden und Attribute. Beispielsweise gibt es das Objekt »struct crypto_tfm« unter anderem in einer Ausprägung für den synchronen (»struct crypto_blkcipher« ) und einer für den asynchronen Zugriff (»struct crypto_ablkcipher« ).

Abbildung 4: Die Datenstrukturen des Crypto-Subsystems sind unübersichtlich und verschachtelt.

Abbildung 4: Die Datenstrukturen des Crypto-Subsystems sind unübersichtlich und verschachtelt.

Header-Studium

Um sich hier zum gewünschten Element durchzuhangeln, hilft nur, die zugehörigen Deklarationen in der Headerdatei »<linux/crypto.h>« gründlich zu studieren [3]. Diese Headerdatei stellt auch einige Inline-Funktionen wie beispielsweise »crypto_tfm_ctx()« (Listing 2, Zeile 31) zur Verfügung, die den Zugriff auf die Elemente abstrahieren. Abbildung 5 zeigt, wie »make« aus den beiden Codebeispielen zwei Kernelmodule generiert. Nacheinander geladen – erst der Verschlüsselungsalgorithmus, danach das zugreifende Testmodul – entschlüsseln sie den Cyphertext. An dieser Stelle noch einmal der Hinweis: Der vorgestellte Verschlüsselungsalgorithmus dient ausschließlich zu Demonstrationszwecken und gewährleistet in der vorgestellten Form keinen Schutz! Die implementierte XOR-Verschlüsselung ist nur dann sicher, wenn der Schlüssel mindestens ebenso lang wie die Nachricht ist und außerdem die Schlüsselwerte zufällig verteilt sind. Im Ernstfall greift der Programmierer also lieber zu etwas Bewährtem wie AES mit einem ausreichend langen Schlüssel.

Abbildung 5: Zwei Module entschlüsseln die Geheimnachricht.

Abbildung 5: Zwei Module entschlüsseln die Geheimnachricht.

Das Crypto-Subsystem des Linux-Kernels bietet noch weit mehr Funktionalitäten als diese und die vorige Kern-Technik beschrieben haben. Dazu gehören beispielsweise der Umgang mit Initialisierungsvektoren über so genannte Templates oder das automatische Laden eines Transformations-Moduls. Offene Fragen kann der Programmierer aber leider fast ausschließlich durch Quellcodestudium klären. (mhu)

Für Kommentare

Infos

  1. Jürgen Quade und Eva-Katharina Kunst, “Kern-Technik”, Folge 57: Linux-Magazin 06/11, S. 86
  2. Sebastian Siewior, “Acceleration of encrypted communication using co-processors”: http://diploma-thesis.siewior.net/html/diplomarbeitch4.html
  3. 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. Zwischenzeitlich ist die dritte Auflage ihres Buches “Linux Treiber entwickeln” erschienen.

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
Nach oben