Artikel

Lizenzschlüssel selbst bauen — und was daran heikel ist

Ein Schlüssel im Format BLACK-XXXXX-ZACK-XXXXX-XXXXX, eine Prüfung, ein Gerätebezug. Wie ich ein Lizenzsystem gebaut habe und warum der schwierigste Teil nicht die Kryptografie ist.

BlackZackBlackzack

960 Wörter · 5 Min. Lesezeit

  • lizenzen
  • sicherheit
  • architektur

Mit dem Premium-Modell kam die Frage nach Lizenzen — nicht nur für den Bot selbst, sondern auch für Software, die ich weitergebe. Ein Lizenzsystem klingt nach einem gelösten Problem, und in den Grundzügen ist es das. Interessant sind die Entscheidungen drumherum.

Das Format

Ich habe mich für ein Muster entschieden, das man am Telefon vorlesen und in ein Formular tippen kann:

BLACK-4F2A9-ZACK-7C1D3-88BE5

Fünf Blöcke, feste Präfixe, Großbuchstaben und Ziffern. Drei Details, die sich bewährt haben:

Ein eingeschränkter Zeichensatz. Kein O, kein 0, kein I, kein 1. Diese vier Zeichen sind die häufigste Fehlerquelle beim Abtippen. Der Verlust an Kombinationen ist bedeutungslos, der Gewinn an Support-Ruhe erheblich.

Eine Prüfziffer. Der letzte Block enthält eine Prüfsumme über die vorherigen. Damit kann man einen Tippfehler sofort erkennen, ohne die Datenbank zu fragen — und Zufallseingaben werden abgewiesen, bevor sie irgendwo Last erzeugen.

Ein festes Präfix. Ein Schlüssel ist als solcher erkennbar. Das hilft im Support und, unerwartet nützlich, beim Suchen nach versehentlich veröffentlichten Schlüsseln.

Was in der Datenbank steht — und was nicht

Der wichtigste Punkt, und der, bei dem ich zuerst falsch abgebogen bin: Der Schlüssel selbst gehört nicht im Klartext in die Datenbank. Gespeichert wird ein Hash, genau wie bei Passwörtern. Bei der Prüfung wird der eingegebene Schlüssel gehasht und verglichen.

Der Preis dafür ist, dass man einem Kunden seinen Schlüssel nicht noch einmal anzeigen kann. Genau deshalb hatte ich zunächst Klartext gespeichert. Die Lösung ist nicht, das Geheimnis lesbar zu halten, sondern es dorthin zu schicken, wo es hingehört — in die Kaufbestätigung und in den Kundenbereich, in dem man einen neuen Schlüssel erzeugen lassen kann.

CREATE TABLE lizenzschluessel (
  id           INTEGER PRIMARY KEY,
  schluessel_hash TEXT NOT NULL UNIQUE,
  produkt      VARCHAR(40) NOT NULL,
  besitzer     VARCHAR(40) NOT NULL,
  status       VARCHAR(20) NOT NULL DEFAULT 'aktiv',
  gueltig_bis  DATETIME NULL,
  max_geraete  INT NOT NULL DEFAULT 1,
  erstellt_am  DATETIME NOT NULL
);

Der Gerätebezug — die eigentliche Schwierigkeit

Eine Lizenz soll nicht beliebig oft benutzt werden. Also braucht es einen Bezug zu etwas: einem Rechner, einer Installation, einem Server.

Hier fangen die Probleme an, und sie sind nicht technischer Natur:

Ein Gerätemerkmal ändert sich. Festplatte getauscht, Netzwerkkarte gewechselt, Container neu gebaut — und plötzlich ist es ein anderes Gerät. Wer daraus eine harte Sperre macht, produziert Support-Fälle bei ehrlichen Kunden.

Ein Gerätemerkmal ist fälschbar. Alles, was der Client berechnet und schickt, kann er auch erfinden. Das ist kein Argument dagegen, es zu benutzen — aber eins dagegen, sich darauf zu verlassen.

Zu strenge Bindung schadet den Falschen. Wer eine Lizenz weitergeben will, findet einen Weg. Wer ehrlich ist und umzieht, steht vor einer Sperre. Das Verhältnis ist ungünstig.

Meine Lösung war eine weiche: Eine Lizenz erlaubt eine feste Zahl gleichzeitiger Installationen. Neue Installationen werden registriert, alte lassen sich freigeben, und der Zähler kann vom Support zurückgesetzt werden. Es gibt eine Prüfspur, wer wann was zurückgesetzt hat.

Infobox

Der entscheidende Gedanke: Ein Lizenzsystem hält niemanden auf, der entschlossen ist. Es sorgt dafür, dass der ehrliche Weg der einfachere ist, und dass man weiß, wer welche Lizenz benutzt. Wer ein System baut, das gegen entschlossene Angreifer wirken soll, baut ein System, das ehrliche Kunden quält — und wird trotzdem umgangen.

Der Ablauf der Prüfung

Client  →  POST /lizenz/pruefen  { schluessel, installation }
Server  ←  { status: "aktiv", gueltig_bis, funktionen: [...] }

Vier Punkte, die dabei wichtig sind:

Signieren. Die Antwort wird signiert, damit der Client prüfen kann, dass sie echt ist. Sonst genügt ein umgebogener Eintrag in der Namensauflösung, um jede Antwort zu fälschen.

Zeitgrenzen setzen. Eine Antwort gilt eine begrenzte Zeit. Damit kann sie nicht dauerhaft wiederverwendet werden.

Ausfall bedenken. Was passiert, wenn der Lizenzserver nicht erreichbar ist? Wenn die Software dann streikt, macht ein Ausfall bei mir die Software bei allen kaputt. Ich habe eine Karenzzeit eingebaut: Die letzte gültige Antwort trägt eine Weile weiter.

Protokollieren. Jede Prüfung wird mitgeschrieben — grob, ohne persönliche Details. Das ist die Grundlage für die Frage, ob ein Schlüssel plötzlich von zwanzig Orten gleichzeitig benutzt wird.

Was beim Ausliefern von Schlüsseln zu beachten ist

Ein Schlüssel entsteht nicht nur, er muss auch zum Käufer gelangen — und dieser Weg hat eigene Fallstricke.

Die Zustellung darf nicht der einzige Weg sein. Eine E-Mail kann im Spam landen, eine Chat-Nachricht ungelesen bleiben. Es braucht einen Kundenbereich, in dem der Schlüssel jederzeit abrufbar ist.

Ein Schlüssel im Chatverlauf ist ein veröffentlichter Schlüssel. Wer ihn per Nachricht schickt, muss damit rechnen, dass er in einem Bildschirmfoto landet. Ein Verweis auf den Kundenbereich ist die bessere Zustellung.

Der Zeitpunkt zählt. Ein Schlüssel, der vor der Zahlungsbestätigung erzeugt wird, existiert auch dann, wenn die Zahlung scheitert. Deshalb: erzeugen erst nach Bestätigung, und bei Rückbuchung ausdrücklich widerrufen.

Der Teil, den ich falsch gebaut hatte

Meine erste Fassung speicherte den Schlüssel im Klartext. Der Grund war ein nachvollziehbarer Wunsch: Ich wollte im Support sagen können, welcher Schlüssel zu welchem Kunden gehört.

Die Lösung dafür ist nicht Klartext, sondern ein zusätzliches Feld mit einem Erkennungsteil — die ersten und letzten Zeichen, gespeichert getrennt vom Hash:

schluessel_hash:   <Hash des vollständigen Schlüssels>
schluessel_anzeige: BLACK-4F2A9-…-88BE5

Damit kann ich einen Schlüssel in einer Liste wiedererkennen und mit einem Kunden abgleichen, ohne dass der vollständige Wert irgendwo liegt. Der Erkennungsteil allein ist nicht ausreichend, um einen gültigen Schlüssel zu raten.

Dieses Muster — Hash für die Prüfung, Ausschnitt für die Wiedererkennung — benutze ich seitdem für alles, was geheim ist und trotzdem im Support besprochen werden muss. Es ist derselbe Gedanke wie bei der Maskierung von Zugangsdaten, nur an einer anderen Stelle.

Was ich Jahre später darüber gelernt habe

Diese Grundlage hat sich als eine der langlebigsten Sachen erwiesen, die ich gebaut habe. Ich habe später ein fremdes Plugin gegen dieselbe Infrastruktur laufen lassen, ohne das Plugin selbst anzufassen — weil dessen Prüfprotokoll ähnlich aufgebaut war und der Serverteil austauschbar blieb.

Was ich dabei zusätzlich gelernt habe: Wenn eine Lizenzprüfung über unverschlüsseltes HTTP läuft und die Antwort nicht signiert ist, ist sie im Grunde eine Höflichkeitsabfrage. Die eigentliche Absicherung ist dann nicht das Protokoll, sondern das Netz, in dem es stattfindet. Diese Unterscheidung — ob Sicherheit im Verfahren oder in der Erreichbarkeit steckt — ist mir seitdem bei jedem System wichtig.