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).
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.
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.
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).