Froxlor, Sogo, Prosody und Letsencrypt vereinigt

Nachdem Squeeze LTS nun auch EOL ist, wurde es an der Zeit einen neuen Hostingserver aufzusetzen, der meine ganzen Internetservices vorhält. Diesmal wollte ich gleich anfangs alles schön integrieren, so dass keine Tweaks notwendig sind, um das Ganze zu pflegen. Ziel dabei war es, alle Dienste, die ein User zurVerfügung hat gegen die gleiche Datenbank authentifizieren, nämlich die Froxlor Mail_Users Table.

Um welche Dienste handelt es sich?

Für mich beinhaltet ein Standardsetup folgende Dienste:

  1. Mail ( IMAP, POP3, Sieve)
  2. Groupware mit Cal/CardDAV Support sowie Webfrontend mit Sogo
  3. Instant Messaging Account mit Prosody

1. Schritt Sogo

Sogo ist eine Groupware, bei der es sich um eine Weiterentlickung von OpenGroupware handelt. Ich setzte diese Goupware seit Jahren sehr zufriedenstellend ein, sie funktioniert einfach.

Nun bietet Sogo an, die Authentifizierung gegen LDAP oder SQL zu machen. Zudem wird auch der von mir in Froxlor eingesetzte Hashalgo SSHA512 unterstützt. Also einfach mal eine View gebaut, die die Daten für Sogo passend vorhält:

select `froxlor`.`mail_users`.`email` AS `c_uid`,
       `froxlor`.`mail_users`.`email` AS `c_name`,
       `froxlor`.`mail_users`.`password_enc` AS `c_password`,
       `froxlor`.`mail_users`.`email` AS `c_cn`,
       `froxlor`.`mail_users`.`email` AS `mail` 
       
       from `froxlor`.`mail_users` where (substr(`froxlor`.`mail_users`.`email`,(locate('@',`froxlor`.`mail_users`.`email`) + 1)) = 'example1.com')

Leider war dieser Versuch zum Scheitern verurteilt. Nach Durchschauen der Logs wurde auch schnell klar warum. Froxlor legt den Passworthash in einem Format ab, dass man in /etc/shadow vielleicht schon einmal gesehen hat und von dem ich mittlerweile weiss, das es sich um das  MCF  Format ( modular crypt format ) handelt.

Schaut man bei Sogo in die Docs findet man aber nur Erwähnung einer anderen Art den Hash abzulegen, nämlich am Anfang in geschweiften Klammern der Typ des Hashs und anschliesend der Hash selbst, salted und Base64 encoded.

Die Probleme gingen also los und ich bin erst einmal nicht wirklich weiter gekommen, da über die Details leider wieder sehr wenig Infos zu finden sind.

Ich habe dann mit kleinen Codestücken geschaut, wie ich das Ganze zusammenbekommen und letztendlich gab es nur die Lösung:

Das Passwort muss noch in einem anderen Format abgelegt werden

Dabei ist natürlich darauf zu achten, dass auch immer beide Felder beim Passwortwechsel aktualisiert werden. Hierzu bin ich durch die Froxlor Sources durchgegegangen, um herauszufinden  das die Datei customer_email.php den relevanten Code enthält. Ich habe mir jetzt zunutze gemacht, dass Froxlor die Option anbietet, dass Passwort in Klartext abzulegen. Diese Option hat durchaus seine Berechtigung, Klartext ist allerdings für mich ein NoGo, also war es deaktiviert.

Damit ich nun meinen Passworthash dort ablegen kann, habe ich die Sourcen gepatched:

root@master:~/src/Froxlor# diff customer_email.php /var/www/froxlor/customer_email.php 
457a458,459
> $ssha512_salt = substr(str_replace('+','.',base64_encode(md5(mt_rand(), true))),0,16);
> $password_ssha512b64 = "{SSHA512.b64}" . base64_encode(hash('sha512', $password . $ssha512_salt, true) . $ssha512_salt);
484c486
< if (Settings::Get('system.mailpwcleartext') == '1') { $params["password"] = $password; } --- > if (Settings::Get('system.mailpwcleartext') == '1') { $params["password"] = $password_ssha512b64; }
646a649,650
> $ssha512_salt = substr(str_replace('+','.',base64_encode(md5(mt_rand(), true))),0,16);
> $password_ssha512b64 = "{SSHA512.b64}" . base64_encode(hash('sha512', $password . $ssha512_salt, true) . $ssha512_salt);
658c662
< if (Settings::Get('system.mailpwcleartext') == '1') { $params["password"] = $password; } --- > if (Settings::Get('system.mailpwcleartext') == '1') { $params["password"] = $password_ssha512b64; }

Ist in Froxlor nun die Klartextpasswortablage aktiviert, wird in das Feld „password“ nun das Passwort  in meinem gewünschten Format abgelegt. Hier mal ein Beispielhash:

{SSHA512.b64}fuN89Rh0/H1we4Qf18+QAB9hFIndPsGykagJ8Zs8Dk3UXsN2ia3Fv/lwlAg3QfWS6qjW/LFmoHu/KIth4Mvl12FHai94S3pHRlQxTXFEXm0=

In diesem Hashformat hat Sogo keinerlei Probleme die Authentifizierung auszuführen, so dass dann mit folgender Domainkonfiguration und der Anpassung der View ( password anstatt password_enc und natürlich eine View pro Domain) Sogo bereits fertig integriert ist:

  domains = {
      example1.com = {
          SOGoMailDomain = example1.com;
          SOGoUserSources = (
                {
                  type = sql;
                  id = example1;
                  viewURL = "mysql://froxlor:PASSWORD@localhost:3306/froxlor/sogo_example1_view";
                  canAuthenticate = YES;
                  isAddressBook = NO;
                  userPasswordAlgorithm = "ssha512.b64";
                }
          );
      };
      example2.com = {
              SOGoMailDomain = example2.com;
              SOGoUserSources = (
                   {
                     type = sql;
                     id = example2;
                     viewURL = "mysql://froxlor:PASSWORD@localhost:3306/froxlor/sogo_example2_view";
                     canAuthenticate = YES;
                     isAddressBook = NO;
                     userPasswordAlgorithm = "ssha512.b64";
                   }
              );
      };
  };

2. Schritt Prosody

Auch hier besteht die Schwierigkeit darin zu wissen, was eigentich gemacht wird. Es gibt bereits ein Plugin, um gegen ein Klartextpasswort in SQL zu authentifizieren. Leider habe ich keine Doku dazu gefunden, wie denn solch ein Plugin genau definiert ist und wie die Schnittstellen sind. Zudem das Ganze noch in Lua, mein erster Kontakt mit der Sprache abgesehen vom Editieren der Prosody Config :-).
Letztendlich habe ich die Lösung durch probieren und anschauen einiger anderer Auth Plugins erreicht. Folgende Plugins haben mir geholfen:

Man findet die Auth Plugins hier

Mein erstes Problem war natürlich Lua, das ich bisher keine Ahnung von der Sprache hatte und meine Goto Scriptingsprache Python ist. Ist man das „batteries included“ gewöhnt, schaut man als erstes mal, was die Standardbibliothek einem so als Lösungsansatz bietet. Da ist man bei Lua erst einmal aufgeschmissen, zumindest ich, der vielleicht noch nicht weiss, wo die guten Infos zu finden sind.

Beim Suchen der von mir benötigten Funktionen – Base64 Encode/Decode, SHA2 Hash – bin ich schlieslich auf  luarocks gestossen.

luarocks install sha2

Bringt mir den nötigen Hashalgo.
Im LuaUsers Wiki habe die Base64 Funktionen gefunden.

Mit allen benötigten Funktionen ging es daran, meine Teile so zusammen zu pasten, dass es am Ende funktioniert. Dafür habe ich ein kleines Luascript geschieben und solange rumprobiert, bis ich mit dem gleichen Salt, gleiche Hashes in PHP und Lua generieren konnte.

Hier mal die entscheidenden Codefragmente:

local function SHA512b64HashPassword(plaintext, salt)
	local hash = '{SSHA512.b64}'..enc(sha2.sha512(plaintext..salt)..salt);
	log("debug", "Hash: %s", hash);
	return hash;
end
local function SHA512b64CheckHash(password, hash)
	local salt = string.sub(dec(string.sub(hash,13)),65)
        local phash = tostring(SHA512b64HashPassword(password,salt));
        log("debug", "*****Phash: %s", phash);
	log("debug", "*****Hash: %s", ahash);
        log("debug", "Salt: %s", salt);
        if hash == phash then
		log("debug", "Auth successfull");
		return true;
	else
                log("debug", "Auth failed");
                return false;
	end;

end

Letzendlich habe ich daraus dann ein eigens Prosody Auth Modul gemacht, mod_auth_sqlsha2. Es funktioniert bereits, ich muss aber auf jeden Fall noch einmal abchecken, wie saubere Fehlerbehandlung in Lua funktioniert. Zudem ist momentan nur die Authentifizierung umgesetzt, da ich aus dem SASL interface nicht wirklich schlau geworden bin.

3. Letsencrypt

Dieser Teil ist noch nicht vollständig integriert, da erst die nächste Froxlorversion eine Integration mitbringt. Das LEtsencrypt Tool bringt ja verschiedene Plugins mit und Ziel ist es natürlich, neue Zertifikate ohne Herunterfahren des Webservers hinzubekommen.
Dazu gibt es einen sehr interessanten Ansatz, dass Webroot Plugin zu benutzen und dann in jedem Vhost einen Alias zu setzen: Using Free SSL/TLS Certificates from Let’s Encrypt with NGINX